AngularJS - set validity of wrapped directives - angularjs

I'm working on a simple directive - date/time picker. It wraps angular material datepicker and two additional selects (for hours and minute), builds the date and exposes it via ngModel.
I have implemented validation logic, where user can supply min and max value, and if the selected date and time falls outside range, model validity is set to false.
Here is the markup of the directive and it's code:
<div layout="column">
<div layout="row">
<md-datepicker name="selectedDatePicker" ng-model="selectedDate" flex="50" flex-order="1"></md-datepicker>
<md-input-container flex="25" flex-order="2">
<label>Hour</label>
<md-select ng-model="selectedHour">
<md-option ng-repeat="hour in hours" value="{{hour}}">
{{hour}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex="25" flex-order="2">
<label>Minute</label>
<md-select ng-model="selectedMinute">
<md-option ng-repeat="minute in minutes" value="{{minute}}">
{{minute}}
</md-option>
</md-select>
</md-input-container>
</div>
<div layout="row">
<ng-transclude></ng-transclude>
</div>
</div>
angular.module('myApp')
.directive('jcdatetimepicker', function ($parse) {
var link = function (scope, iElement, iAttrs, ngModel) {
var initValue = scope.ngModel;
setDateTime(initValue);
ngModel.$parsers.push(validateInput);
ngModel.$formatters.push(validateInput);
ngModel.$render = function () {
setDateTime(ngModel.$viewValue);
}
scope.$on('jcdatetimepicker:updateModel', function (evt, args) {
ngModel.$setViewValue(validateInput(args));
});
function validateInput(inputDate){
var valid = true;
if (angular.isDefined(scope.minValue) && (inputDate < scope.minValue)) {
valid = false;
ngModel.$setValidity('min', false);
}
else {
ngModel.$setValidity('min', true);
}
if (angular.isDefined(scope.maxValue) && (inputDate > scope.maxValue)) {
valid = false;
ngModel.$setValidity('max', false);
}
else {
ngModel.$setValidity('max', true);
}
return valid ? inputDate : undefined;
}
function setDateTime(date) {
if (angular.isDate(date)) {
scope.selectedMinute = date.getMinutes();
scope.selectedHour = date.getHours();
scope.selectedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
}
};
return {
restrict: 'E',
replace: false,
transclude: true,
require: '^ngModel',
scope: {
ngModel: '=ngModel',
minValue: '=min',
maxValue: '=max'
},
controller: 'jcDateTimePickerController',
link: link,
templateUrl: 'templates/shared/jcDateTimePicker/jcDatetimePicker.html'
}
}).controller('jcDateTimePickerController', function ($scope, $element) {
$scope.hours = [];
$scope.minutes = [];
$scope.selectedMinute = 0;
$scope.selectedHour = 12;
$scope.selectedDate = new Date();
$scope.isValid = false;
//initialize hours & minutes
function init() {
for (var h = 0; h < 24; h++) {
$scope.hours.push(h);
}
for (var m = 0; m < 60; m++) {
$scope.minutes.push(m);
}
};
init();
$scope.$watchGroup(['selectedMinute', 'selectedHour', 'selectedDate'], function (newVals, oldVals, _scope) {
var minutes = newVals[0];
var hours = newVals[1];
var date = newVals[2];
date.setHours(hours, minutes, 0);
$scope.$broadcast('jcdatetimepicker:updateModel', date);
});
});
It works fine when it comes to validation - ngModel validity of model passed to my directive is set correctly. The problem is, that when the selected value is not valid, I would like also to set validity on the directives that my directive is wrapping mdDatepikcer and mdSelect - so that UI of those directives will also indicate invalid input.
However I have no clue how to grab those wrapped directives, e. g. date picker and tinker with it's validity. In unit tests, I tried with isolated scope, but selectedDatePicker was undefined.
Is that even possible? Or is it possible to get it's ngModel controller, and set validity that way?
Any advice is highly appreciated.

if you have the signature of the scopes of the mdDatepicker and mdSelect, you can use: angular.element($("[name='selectedDatePicker']")).scope, then call the function(s) or set properties that's necessary to give up the behavior you want.
I haven't tried the code but that's one possible way.

Related

Angular bootstrap datepicker isnt loading in a separate screen

I have a date picker in one screen/url (in the same app) where the template works and loads properly but now in a different screen/url (in the same app) the date picker doesnt display properly. It looks like:
Ive compared both codes from both screens and its not working..
function dateFilter() {
var directive = {
controller: dateFilterController,
controllerAs: 'vm',
bindToController: true,
restrict: 'E',
templateUrl: 'app/utils/directives/date-filter/date.filter.tpl.html',
scope: {
'popup': '=',//date format
'options': '=',//uib-datepicker options
'ngModel': '=',//selected date
'ngChange': '=',//filter function
'myId': '=',//id and name of uib-datepicker
'label': '=',//label that preceeds datepicker
'name': '=',//name of the property being filtered
'minDate': '=',//the minumum acceptable date
'maxDate': '=',//the maximum acceptable date,
'updateQuery': '=',
'query': '=',
'popupUrl': '='
}
};
return directive;
}
function dateFilterController() {
var vm = this;
vm.dateChanged = function (date) {
var newFilterValue = {
query: vm.query,
value: date
};
vm.updateQuery(newFilterValue);
};
vm.applyDateFilter = function (x) {
vm.dateChanged(x);
var from = true;
//If the label ends in to, that means we are filtering on the to date.
if (vm.label.substring(vm.label.length - 2, vm.label.length) === 'to') {
from = false;
}
//format the filter data in a way so it can be interpretted by ngChange
var temp = {
key: vm.name,//The name of the property being filtered
value: x,//The value from the filter
from: from//Boolean indicating if it is the to(false) or from(true) date.
};
vm.ngChange(temp);
};
vm.isOpen = false;
vm.ngClick = function () {
vm.isOpen = true;
};
}
This is the date filter tpl
<div class="date_input">
<label for="{{vm.myId}}">{{ vm.label | translate}}</label>
<input tabindex="0"
role="button"
aria-labelledby="fromToDescription"
id="{{vm.myId}}"
name="{{vm.myId}}"
type="text"
class="form-control"
uib-datepicker-popup="{{vm.popup}}"
datepicker-options="vm.options"
datepicker-popup-template-url="{{vm.popupUrl}}"
placeholder="mm/dd/yyyy" ng-pattern="/^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(20)\d{2}$/"
autocomplete="off"
ng-model="vm.ngModel"
is-open="vm.isOpen"
show-button-bar="false"
ng-change="vm.applyDateFilter(vm.ngModel)"
ng-click="vm.ngClick($event)"
minDate="vm.minDate"
maxDate="vm.maxDate"
/>
</div>
html where I used the directive.
<!-- FROM DATE -->
<date-filter
popup="$ctrl.formats"
options="$ctrl.dateOptions"
ng-model="$ctrl.fromDate"
ng-change="$ctrl.filterFunction"
name="customFromDate"
label="$ctrl.fromVerbiage"
min-date="$ctrl.dateOptions.minDate"
max-date="$ctrl.dateOptions.maxDate"
my-id="customFromDate"
update-query="$ctrl.updateQuery"
query="customFromDate"
popup-url="$ctrl.popupTpl"
></date-filter>
<div> <!-- TO DATE -->
<date-filter
popup="$ctrl.formats"
options="$ctrl.dateOptions"
ng-model="$ctrl.toDate"
ng-change="$ctrl.filterFunction"
name="customToDate"
label="$ctrl.toVerbiage"
min-date="$ctrl.dateOptions.minDate"
max-date="$ctrl.dateOptions.maxDate"
my-id="customToDate"
update-query="$ctrl.updateQuery"
query="customToDate"
popup-url="$ctrl.popupTpl"
></date-filter>
</div>

Directive function not running

I have this directive:
angular.module('colab.dropdown', []).directive('colabDropdown', function(){
return {
restrict: 'A',
transclude: true,
scope: {
ddlist: '=listtoshow',
model: '=modeltosave',
displayField: '=listfieldtoshow',
fieldtoSave: '=listfieldtosave'
},
link: function(scope, element, attrs) {
var ddlist = scope.ddlist,
model = scope.model,
displayField = scope.displayField,
fieldtoSave = scope.fieldtoSave,
rowIdx = scope.rowIndex;
rowIdx = null;
for(var i=0;i < ddlist.length;i++){
if(ddlist[i][attrs.listfieldtosave] == model){
rowIdx = i;
}
}
if(rowIdx !== null){
scope.displayField = ddlist[rowIdx][attrs.listfieldtoshow];
}
console.log(scope.displayField);
scope.selectItem = function(unit) {
console.log(unit);
// var idx = scope[attrs.listtoshow].indexOf(unit);
// scope[attrs.modeltosave] = unit[attrs.listfieldtosave];
// scope.listfieldtoshow = unit[attrs.listfieldtoshow];
};
}
}
});
here is my template:
<div class="radio">
<label>
<input type="radio"
name="chickenEgg"
ng-value="mm.id"
ng-model="formData.chickenEgg"
ng-change="selectItem(mm)"
>
{{mm.name}} - {{mm.year}}
</label>
</div>
I am not able to do the following:
run function selectedItem() in my template when I click the radio.
display the {{displayField}} in the DOM. I can display it in the console (line 38 of the directive) but not in the DOM. see the plunker console**
I can run the selectItem() if I don't have the scope:{...} in the directive.
http://plnkr.co/edit/wuLmJs5f6hVhPEP7OqKS?p=preview
Thank you.
You need to add you add your selectItem() function to angular-hovercard.js file.
$scope.selectItem = function(item) {
console.log(item);
}
Check it Out

Directive for comparing two dates

I have used following code for directive which compares two dates (reference Custom form validation directive to compare two fields)
define(['./module'], function(directives) {
'use strict';
directives.directive('lowerThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
ctrl.$setValidity('lowerThan', false);
var validate = function(viewValue) {
var comparisonModel = $attrs.lowerThan;
/*if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
//console.log("It's valid because we have nothing to compare against");
ctrl.$setValidity('lowerThan', true);
}*/
// It's valid if model is lower than the model we're comparing against
//ctrl.$setValidity('lowerThan', parseInt(viewValue, 10) < parseInt(comparisonModel, 10) );
if(comparisonModel){
var to = comparisonModel.split("-");
var t = new Date(to[2], to[1] - 1, to[0]);
}
if(viewValue){
var from=viewValue.split("-");
var f=new Date(from[2],from[1]-1,from[0]);
}
console.log(Date.parse(t)>Date.parse(f));
ctrl.$setValidity('lowerThan', Date.parse(t)>Date.parse(f));
return viewValue;
};
ctrl.$parsers.unshift(validate);
ctrl.$formatters.push(validate);
$attrs.$observe('lowerThan', function(comparisonModel){
// Whenever the comparison model changes we'll re-validate
return validate(ctrl.$viewValue);
});
};
return {
require: 'ngModel',
link: link
};
}
]);
});
but when page is loaded first time it displays error message. i have tried using ctrl.$setValidity('lowerThan', false); to make it invisible first time. but it is not working.
Here is plunker for the same.
http://plnkr.co/edit/UPN1g1JEoQMSUQZoCDAk?p=preview
Your directive is fine. You're setting your date values inside the controller, and you're setting the lower date to a higher value, which means the dates are invalid on load. Your directive correctly detects that. If you don't want your directive to validate your data on load, than you'll need three things:
Remove the $attrs.$observe
Create and apply a higherThan directive to the other field
Tell your directive not to apply to the model value ($formatters array) but only to the input value ($parsers array).
PLUNKER
'use strict';
var app = angular.module('myApp', []);
app.controller('MainCtrl', function($scope) {
$scope.field = {
min: "02-04-2014",
max: "01-04-2014"
};
});
app.directive('lowerThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
var validate = function(viewValue) {
var comparisonModel = $attrs.lowerThan;
var t, f;
if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
ctrl.$setValidity('lowerThan', true);
}
if (comparisonModel) {
var to = comparisonModel.split("-");
t = new Date(to[2], to[1] - 1, to[0]);
}
if (viewValue) {
var from = viewValue.split("-");
f = new Date(from[2], from[1] - 1, from[0]);
}
ctrl.$setValidity('lowerThan', Date.parse(t) > Date.parse(f));
// It's valid if model is lower than the model we're comparing against
return viewValue;
};
ctrl.$parsers.unshift(validate);
//ctrl.$formatters.push(validate);
};
return {
require: 'ngModel',
link: link
};
}
]);
app.directive('higherThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
var validate = function(viewValue) {
var comparisonModel = $attrs.higherThan;
var t, f;
if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
ctrl.$setValidity('higherThan', true);
}
if (comparisonModel) {
var to = comparisonModel.split("-");
t = new Date(to[2], to[1] - 1, to[0]);
}
if (viewValue) {
var from = viewValue.split("-");
f = new Date(from[2], from[1] - 1, from[0]);
}
ctrl.$setValidity('higherThan', Date.parse(t) < Date.parse(f));
// It's valid if model is higher than the model we're comparing against
return viewValue;
};
ctrl.$parsers.unshift(validate);
//ctrl.$formatters.push(validate);
};
return {
require: 'ngModel',
link: link
};
}
]);
<form name="form" >
Min: <input name="min" type="text" ng-model="field.min" lower-than="{{field.max}}" />
<span class="error" ng-show="form.min.$error.lowerThan">
Min cannot exceed max.
</span>
<br />
Max: <input name="max" type="text" ng-model="field.max" higher-than="{{field.min}}" />
<span class="error" ng-show="form.max.$error.higherThan">
Max cannot be lower than min.
</span>
</form>

Word counter in angularjs

I'm a newbie in angular so please bear with me. I have a character counter and word counter in my textarea. My problem is that everytime I press the space, it is also being counted by getWordCounter function. How can I fix this? Thank you in advance.
HTML:
<textarea id="notesContent" type="text" class="form-control" rows="10" ng-model="notesNode.text" ng-trim="false" maxlength="5000"></textarea>
<span class="wordCount">{{getWordCounter()}}</span>
<span style="float:right">{{getCharCounter()}} / 5000</span>
JS:
$scope.getCharCounter = function() {
return 5000 - notesNode.text.length;
}
$scope.getWordCounter = function() {
return $.trim(notesNode.text.split(' ').length);
}
It seems like you need to call 'trim' before calling split, like this:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(' ').length;
}
If you want to support multiple spaces between words, use a regular expression instead:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(/\s+/).length;
}
Filter implementation
You can also implement wordCounter as a filter, to make it reusable among different views:
myApp.filter('wordCounter', function () {
return function (value) {
if (value && (typeof value === 'string')) {
return value.trim().split(/\s+/).length;
} else {
return 0;
}
};
});
Then, in the view, use it like this:
<span class="wordCount">{{notesNode.text|wordCounter}</span>
See Example on JSFiddle
This is a more advanced answer for your problem, since it can be reusable as a directive:
var App = angular.module('app', []);
App.controller('Main', ['$scope', function($scope){
var notesNode = {
text: '',
counter: 0
};
this.notesNode = notesNode;
}]);
App.directive('counter', [function(){
return {
restrict: 'A',
scope: {
counter: '='
},
require: '?ngModel',
link: function(scope, el, attr, model) {
if (!model) { return; }
model.$viewChangeListeners.push(function(){
var count = model.$viewValue.split(/\b/g).filter(function(i){
return !/^\s+$/.test(i);
}).length;
scope.counter = count;
});
}
};
}]);
And the HTML
<body ng-app="app">
<div ng-controller="Main as main"></div>
<input type="text" ng-model="main.notesNode.text" class="county" counter="main.notesNode.counter">
<span ng-bind="main.notesNode.counter"></span>
</body>
See it in here http://plnkr.co/edit/9blLIiaMg0V3nbOG7SKo?p=preview
It creates a two way data binding to where the count should go, and update it automatically for you. No need for extra shovelling inside your scope and controllers code, plus you can reuse it in any other input.

Angular.js: choosing a pre-compiled template depending on a condition

[disclaimer: I've just a couple of weeks of angular behind me]
In the angular app I'm trying to write, I need to display some information and let the user edit it provided they activated a switch. The corresponding HTML is:
<span ng-hide="editing" class="uneditable-input" ng:bind='value'>
</span>
<input ng-show="editing" type="text" name="desc" ng:model='value' value={{value}}>
where editing is a boolean (set by a switch) and value the model.
I figured this is the kind of situation directives are designed for and I've been trying to implement one. The idea is to precompile the <span> and the <input> elements, then choose which one to display depending on the value of the editing boolean. Here's what I have so far:
angular.module('mod', [])
.controller('BaseController',
function ($scope) {
$scope.value = 0;
$scope.editing = true;
})
.directive('toggleEdit',
function($compile) {
var compiler = function(scope, element, attrs) {
scope.$watch("editflag", function(){
var content = '';
if (scope.editflag) {
var options='type="' + (attrs.type || "text")+'"';
if (attrs.min) options += ' min='+attrs.min;
options += ' ng:model="' + attrs.ngModel
+'" value={{' + attrs.ngModel +'}}';
content = '<input '+ options +'></input>';
} else {
content = '<span class="uneditable-input" ng:bind="'+attrs.ngModel+'"></span>';
};
console.log("compile.editing:" + scope.editflag);
console.log("compile.attrs:" + angular.toJson(attrs));
console.log("compile.content:" + content);
})
};
return {
require:'ngModel',
restrict: 'E',
replace: true,
transclude: true,
scope: {
editflag:'='
},
link: compiler
}
});
(the whole html+js is available here).
Right now, the directive doesn't do anything but output some message on the console. How do I replace a <toggle-edit ...> element of my html with the content I define in the directive? If I understood the doc correctly, I should compile the content before linking it: that'd be the preLink method of the directive's compile, right ? But how do I implement it in practice ?
Bonus question: I'd like to be able to use this <toggle-edit> element with some options, such as:
<toggle-edit type="text" ...></toggle-edit>
<toggle-edit type="number" min=0 max=1 step=0.01></toggle-edit>
I could add tests on the presence of the various options (like I did for min in the example above), but I wondered whether there was a smarter way, like putting all the attrs but the ngModel and the editflag at once when defining the template ?
Thanks for any insight.
Here is a tutorial by John Lindquist that shows how to do what you want. http://www.youtube.com/watch?v=nKJDHnXaKTY
Here is his code:
angular.module('myApp', [])
.directive('jlMarkdown', function () {
var converter = new Showdown.converter();
var editTemplate = '<textarea ng-show="isEditMode" ng-dblclick="switchToPreview()" rows="10" cols="10" ng-model="markdown"></textarea>';
var previewTemplate = '<div ng-hide="isEditMode" ng-dblclick="switchToEdit()">Preview</div>';
return{
restrict:'E',
scope:{},
compile:function (tElement, tAttrs, transclude) {
var markdown = tElement.text();
tElement.html(editTemplate);
var previewElement = angular.element(previewTemplate);
tElement.append(previewElement);
return function (scope, element, attrs) {
scope.isEditMode = true;
scope.markdown = markdown;
scope.switchToPreview = function () {
var makeHtml = converter.makeHtml(scope.markdown);
previewElement.html(makeHtml);
scope.isEditMode = false;
}
scope.switchToEdit = function () {
scope.isEditMode = true;
}
}
}
}
});
You can see it working here: http://jsfiddle.net/moderndegree/cRXr6/

Resources