Remove $watch from this directive - angularjs

I've wrote this directive to handle my date inputs: https://plnkr.co/edit/7hpc8u5pVc7iaNSwn7Zw?p=preview
app.directive('myDate', ['$filter', myDate]);
function myDate($filter) {
var directive = {
restrict: 'E',
template: template,
require: 'ngModel',
scope: {},
link: link
}
return directive;
function template(element, attrs) {
var template = '<input ng-model="date" ng-keyup="keyup($event.keyCode)" ui-mask="99/99/9999" type="text" ';
if (attrs.class) {
template += 'class="' + attrs.class + '"';
element.removeClass(attrs.class);
}
template += '/>';
return template;
}
function link(scope, element, attrs, ctrl) {
scope.keyup = function(key) {
if (key === 68) { // D key
scope.date = $filter('date')(new Date(), 'ddMMyyyy');
}
};
ctrl.$formatters.push(function(data) { // model to view
data = $filter('date')(data, 'ddMMyyyy');
return data;
});
ctrl.$parsers.push(function(data) { // view to model
var year = data.toString().substr(-4);
var month = data.toString().substr(2, 2);
var day = data.toString().substr(0, 2);
var sep = '-';
data = (year && month && day) ? Date.parse(year + sep + month + sep + day) : '';
return data;
});
scope.$watch('date', function() {
ctrl.$setViewValue(scope.date);
});
ctrl.$render = function() {
scope.date = ctrl.$viewValue;
};
}
}
Unfortunately I used $watch to keep my model updated... I would like to know if there is a better way to trigger $setViewValue(scope.date) without $watch; just to optimize it a bit.
Thank you!

Add this to your template:
ng-change="updateParent()"
And this to you link:
scope.updateParent = function(){
ctrl.$setViewValue(scope.date);
}

Related

Updating model from directive in angularjs

I have a directive to drag and drop.
The drag and drop works well, but I have a problem with updating the model.
After I drop some text into textarea, the text is showing ok, but the model is not updating.
What am I missing here?
//markup
<textarea drop-on-me id="editor-texto" ng-trim="false" ng-model="mymodel"
name="templateSms.template">test.</textarea>
//directive
angular
.module('clinang')
.directive('dragMe', dragMe)
.directive('dropOnMe', dropOnMe);
dragMe.$inject = [];
function typeInTextarea(el, newText) {
var start = el.selectionStart
var end = el.selectionEnd
var text = el.value
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.value = (before + newText + after)
el.selectionStart = el.selectionEnd = start + newText.length
el.focus()
}
function dragMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.prop('draggable', true);
element.on('dragstart', function(event) {
event.dataTransfer.setData('text', event.target.id)
});
}
};
return DDO;
}
dropOnMe.$inject = [];
function dropOnMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('dragover', function(event) {
event.preventDefault();
});
element.on('drop', function(event) {
event.preventDefault();
var data = event.dataTransfer.getData("text");
var x=document.getElementById(data);
typeInTextarea(event.target,x.getAttribute('data-value'))
});
}
};
return DDO;
}
Update your textarea model inside typeInTextarea function and using $apply run digest cycle to update the model change across whole app. For that with your current structure of directives with only link functions you'll need to pass scope to the typeInTextarea function (as a parameter).
So your function will be:
function typeInTextarea(scope, el, newText) {
var start = el.selectionStart
var end = el.selectionEnd
var text = el.value
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.value = (before + newText + after);
scope.mymodel.textnote = el.value;
el.selectionStart = el.selectionEnd = start + newText.length;
el.focus();
}
and dropOnMe function will be:
function dropOnMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('dragover', function(event) {
event.preventDefault();
});
element.on('drop', function(event) {
event.preventDefault();
var data = event.dataTransfer.getData("text");
var x=document.getElementById(data);
typeInTextarea(scope, event.target,x.getAttribute('data-value'))
scope.$apply();
});
}
};
return DDO;
}
Check out this example (I don't know which element you're dragging so e.g. I've considered span element & just used innerHTML for that ):
https://plnkr.co/edit/wGCNOfOhoopeZEM2WMd1?p=preview

How to access an attribute in the directive validator in AngularJS correctly

I'm making a validator which validates valid dates like MM/YYYY, but I didn't get how to access an attribute when the model changes:
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Here is the directive
.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
};
}])
The validator ngModel.$validators.valid works perfect, but inside ngModel.$validators.maxDate i cannot get the attr.maxDate but the first time directive fires.
So how can I access to a custom attribute value every time I check the modelValue?
I'm not an expert with AngularJS and probably I'm missing something important.
The attrs argument in the link function provides you with a $observe method which you can use to attach a listener function for dynamic changes in an attribute value.
It is very simple to use inside of your link function:
attr.$observe('maxDate', function() {
scope.maxDate = attr.maxDate;
ngModel.$validate();
});
Here is a working Plunker
You can do like this for track the change in ng-model:-
HTML
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Angularjs code:-
app.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
$scope.$watch('ngModel',function(){
console.log(attr.dataMaxDate);
});
};
}])

Angular input validation with optional max min always invalid

I am building a directive that adds some logic to the input['date'] element. This is what I have right now:
app.directive('calendarInput', function() {
'use strict';
return {
template : '<input type="date"' +
'ng-model="calendarInput"' +
'min="{{min}}"' +
'max="{{max}}" />'
replace: true,
scope: {
startdate : '#',
enddate : '#',
calendarInput: '='
},
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch('startdate', function (val) {
if (val) {
var date = new Date(val);
scope.min = date.toIsoString().split('T')[0];
}
});
scope.$watch('enddate', function (val) {
if (val) {
var date = new Date(val);
scope.max = date.toIsoString().split('T')[0];
}
});
}
};
});
The idea is to reuse this directive. Sometimes there will be a startdate only, sometimes a enddate only. Like so:
<div calendar-input="item.birthday" enddate="'1990-01-01'"></div>
Unfortunately this always results in an invalid form with the class ng-invalid-min. It's because I don't supply the startdate parameter.
How can I make min or max values optional?
Edit:
I am using Angular 1.3.9
Figured out a way to compile max and min attributes on demand.
app.directive('calendarInput', function($compile) {
'use strict';
return {
template : '<input type="date" class="datepicker" ng-model="omdCalendarInput" />',
replace: true,
scope: {
startdate : '#',
enddate : '#',
calendarInput: '='
},
restrict: 'CA',
link: function (scope, element, attrs) {
var dateInput = element.find('input') // <----
scope.$watch('startdate', function (val) {
if (val) {
var date = new Date(val);
scope.min = date.toIsoString().split('T')[0];
if (!dateInput.attr('min')) { // <----
dateInput.attr('min', '{{min}}'); // <----
$compile(dateInput)(scope); // <----
}
}
});
scope.$watch('enddate', function (val) {
if (val) {
var date = new Date(val);
scope.max = date.toIsoString().split('T')[0];
if (!dateInput.attr('max')) { // <----
dateInput.attr('max', '{{max}}'); // <----
$compile(dateInput)(scope); // <----
}
}
});
}
};
});
Relevant Link
EDIT: Improved the code above. Also I'm marking this as the correct answer, since nobody else has a solution.
This is great, just what I needed. Thank you.
Nit-pick - might want to change:
scope.min = date.toIsoString().split('T')[0];
to
scope.min = $format('date')(date, 'yyyy-MM-dd');
to make it more angularish.

ngModelController: update view value after parser executed

I have the following directive:
myDirectives.directive('simpleDate', function ($filter, $locale, $timeout) {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, elem, attrs, ctrl) {
var dateFormat = 'shortDate';
ctrl.$formatters.unshift(function (modelValue) {
if (!modelValue) {
return '';
} else {
return $filter('date')(modelValue, dateFormat);
}
});
ctrl.$parsers.unshift(function (viewValue) {
if (!isNaN(viewValue)) {
var now = new Date(),
year = now.getFullYear(),
month = now.getMonth() + 1,
day = Number(viewValue),
lastDay = (new Date(year, month, 0)).getDate();
if (day >= 1 && day <= lastDay) {
return '' + year + (month < 10 ? '-0' : '-') +
month + (day < 10 ? '-0' : '-') + day;
}
}
return '';
});
}
};
});
Parser calculates model value, and needs to reflect back view value; but how? I know there is ctrl.$render() method, but I guess it must run after $parser executed. The following didn't work:
$timeout(function () { ctrl.$render(); });
How can I render view value then?
Is this what you expect? :
myDirectives.directive('simpleDate', function ($filter, $locale, $timeout) {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, elem, attrs, ctrl) {
var dateFormat = 'shortDate';
ctrl.$formatters.unshift(function (modelValue) {
if (!modelValue) {
return '';
} else {
return $filter('date')(modelValue, dateFormat);
}
});
ctrl.$parsers.unshift(function (viewValue) {
if (!isNaN(viewValue)) {
var now = new Date(),
year = now.getFullYear(),
month = now.getMonth() + 1,
day = Number(viewValue),
lastDay = (new Date(year, month, 0)).getDate();
if (day >= 1 && day <= lastDay) {
var currentValue = '' + year + (month < 10 ? '-0' : '-') + month + (day < 10 ? '-0' : '-') + day;
ctrl.$setViewValue(currentValue);
ctrl.$render();
return currentValue;
}
}
return '';
});
}
};
});

How to make AngularJS compile the code generated by directive?

Please help me on, How can we make AngularJS compile the code generated by directive ?
You can even find the same code here, http://jsbin.com/obuqip/4/edit
HTML
<div ng-controller="myController">
{{names[0]}} {{names[1]}}
<br/> <hello-world my-username="names[0]"></hello-world>
<br/> <hello-world my-username="names[1]"></hello-world>
<br/><button ng-click="clicked()">Click Me</button>
</div>
Javascript
var components= angular.module('components', []);
components.controller("myController",
function ($scope) {
var counter = 1;
$scope.names = ["Number0","lorem","Epsum"];
$scope.clicked = function() {
$scope.names[0] = "Number" + counter++;
};
}
);
// **Here is the directive code**
components.directive('helloWorld', function() {
var directiveObj = {
link:function(scope, element, attrs) {
var strTemplate, strUserT = attrs.myUsername || "";
console.log(strUserT);
if(strUserT) {
strTemplate = "<DIV> Hello" + "{{" + strUserT +"}} </DIV>" ;
} else {
strTemplate = "<DIV>Sorry, No user to greet!</DIV>" ;
}
element.replaceWith(strTemplate);
},
restrict: 'E'
};
return directiveObj;
});
Here's a version that doesn't use a compile function nor a link function:
myApp.directive('helloWorld', function () {
return {
restrict: 'E',
replace: true,
scope: {
myUsername: '#'
},
template: '<span><div ng-show="myUsername">Hello {{myUsername}}</div>'
+ '<div ng-hide="myUsername">Sorry, No user to greet!</div></span>',
};
});
Note that the template is wrapped in a <span> because a template needs to have one root element. (Without the <span>, it would have two <div> root elements.)
The HTML needs to be modified slightly, to interpolate:
<hello-world my-username="{{names[0]}}"></hello-world>
Fiddle.
Code: http://jsbin.com/obuqip/9/edit
components.directive('helloWorld', function() {
var directiveObj = {
compile:function(element, attrs) {
var strTemplate, strUserT = attrs.myUsername || "";
console.log(strUserT);
if(strUserT) {
strTemplate = "<DIV> Hello " + "{{" + strUserT +"}} </DIV>" ;
} else {
strTemplate = "<DIV>Sorry, No user to greet!</DIV>" ;
}
element.replaceWith(strTemplate);
},
restrict: 'E'
};
return directiveObj;
});
Explanation: The same code should be used in compile function rather than linking function. AngularJS does compile the generated content of compile function.
You need to create an angular element from the template and use the $compile service
jsBin
components.directive('helloWorld', ['$compile', function(compile) {
var directiveObj = {
link: function(scope, element, attrs) {
var strTemplate, strUserT = attrs.myUsername || "";
console.log(strUserT);
if (strUserT) {
strTemplate = "<DIV> Hello" + "{{" + strUserT +"}} </DIV>" ;
} else {
strTemplate = "<DIV>Sorry, No user to greet!</DIV>" ;
}
var e = angular.element(strTemplate);
compile(e.contents())(scope);
element.replaceWith(e);
},
template: function() {
console.log(args);
return "Hello";
},
restrict: 'E'
};
return directiveObj;
}]);

Resources