Angularjs Directive with ngModel - angularjs

I've got a javascript function that we use in a legacy system that alters the format of an input field as you type;
function checkValidDate(dateStr) {
if (dateStr && dateStr != '') {
dateStr = dateStr.replace('/', '');
dateStr = dateStr.replace('/', '');
var d_f_m = dateStr;
var d_f_d = dateStr;
var d_f_y = dateStr;
var err_msg = '';
var d_s_day = d_f_d.slice(0, 2);
d_s_day = d_s_day + "/";
var d_s_month = d_f_m.slice(2, 4);
d_s_month = d_s_month + "/";
var d_s_year = d_f_y.slice(4, 8);
//Now we check the year to see if it is only 2 digis, if is, add 2 more
if (d_s_year.length == 2) {
d_s_year = '19' + d_s_year;
}
return d_s_day + d_s_month + d_s_year;
} else {
return null;
}
}
I've been trying to convert this function to an angularjs directive using ngModel but I just can't seem to sort it out. Would anyone know how to convert this to an angular directive?
Many thanks!

Not sure if you wanted it validated as you type or after you leave a field. Either can work, though the below is implemented to validate after you leave the field (lose focus).
The existing algorithm was used, though it looks like it handles only very specific cases (i.e. 2 digit day, 2 digit month, 2-4 digit year) and could be improved. For the moment, it was copied as is. JSFiddle is here http://jsfiddle.net/pSh4R/18/
HTML:
<div ng-app='app'>
Please enter a date
<br/>
<input type='text' dateformat ng-model='myDate'></input>
(Hit TAB when done)
<hr/>
Model Value : {{myDate}}
</div>
Directive Declaration:
var app = angular.module('app',[]);
app.directive('dateformat', function(){
return {
restrict : 'A',
scope : {
dateStr : '=ngModel'
},
link : function(scope, element){
element.bind('focusout', function (){
scope.$apply(function(){
scope.dateStr = checkValidDate(scope.dateStr);
});
});
function checkValidDate(dateStr) {
if (dateStr && dateStr != '') {
dateStr = dateStr.replace('/', '');
dateStr = dateStr.replace('/', '');
var d_f_m = dateStr;
var d_f_d = dateStr;
var d_f_y = dateStr;
var err_msg = '';
var d_s_day = d_f_d.slice(0, 2);
d_s_day = d_s_day + "/";
var d_s_month = d_f_m.slice(2, 4);
d_s_month = d_s_month + "/";
var d_s_year = d_f_y.slice(4, 8);
//Now we check the year to see if it is only 2 digis, if is, add 2 more
if (d_s_year.length == 2) {
d_s_year = '19' + d_s_year;
}
return d_s_day + d_s_month + d_s_year;
} else {
return null;
}
}
}
};
})

There are a billion ways to do what you want. Here is a quick solution I threw together to get your started:
JS:
var app = angular.module('myApp', []);
app.directive('dateValidator', function() {
return function(scope, element, attrs) {
// Watch for changes to the date model
scope.$watch('date', function(newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) {
var formattedDate = checkValidDate(newVal);
if (formattedDate) {
element.text(formattedDate);
}
}
});
});
});
HTML:
<input type="text" date-validator date="myDate" ng-model="myDate" />

I recommend that you write a custom angular validator. There are some good articles on it. Here is one that I like:
Form Validation with AngularJS
The form and its containing fields have special properties appended to them that you can leverage for binding expressions and client-side validation:
$pristine, $dirty, $valid, $error

Related

Filter on string

I'm learning angularjs and got an exercise that wants me to Use angular filter to show a title in the following format :
first letter of each word upper cased and each other letter lower cased also
remove any non-English letters from the title. For example:
A title with the name
“##THIS is a Title!!”
should be changed to
“This Is A Title”
I'm getting each title from an array of objects and present them like so.
<div ng-repeat="obj in objects">
<h3 class="panel-title">{{obj.Title}}</h3>
</div>
i understand that filter receives an array and filters through it . but this requires me to filter the string.
been searching for a while, how can i do this?
please refer below fiddle
http://jsfiddle.net/HB7LU/28315/
<div ng-controller="MyCtrl">
Hello, {{ name | ordinal|capitalize }}
</div>
var myApp = angular.module('myApp',[]);
//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});
function MyCtrl($scope) {
$scope.name = 'Super hero!!12##3';
}
myApp.filter('ordinal', function() {
// Create the return function
// set the required parameter name to **number**
return function(strTitle) {
// Ensure that the passed in data is a number
// If the data is not a number or is less than one (thus not having a cardinal value) return it unmodified.
strTitle=strTitle.replace(/[^a-zA-Z ]/g, "")
return strTitle;
}
});
myApp.filter('capitalize', function() {
return function(input){
if(input.indexOf(' ') !== -1){
var inputPieces,
i;
input = input.toLowerCase();
inputPieces = input.split(' ');
for(i = 0; i < inputPieces.length; i++){
inputPieces[i] = capitalizeString(inputPieces[i]);
}
return inputPieces.toString().replace(/,/g, ' ');
}
else {
input = input.toLowerCase();
return capitalizeString(input);
}
function capitalizeString(inputString){
return inputString.substring(0,1).toUpperCase() + inputString.substring(1);
}
};
});
angular.module('app', []).filter('myFilter', function(){
return function(input){
if(!input)
return;
var out = '';
var english = /^[A-Za-z0-9 ]*$/;
for(var letter of input)
if(english.test(letter))
out += letter;
var result = '';
for(var i = 0; i < out.length; i++)
result += out[i][(i === 0 || out[i-1] == ' ') ? 'toUpperCase' : 'toLowerCase']();
return result;
}
})
<script src="//code.angularjs.org/snapshot/angular.min.js"></script>
<body ng-app="app">
<input ng-init='text="##THIS is a Title!!"' type='text' ng-model='text'>
<p>{{text | myFilter}}</p>
</body>

Angular ui-bootstrap typeahead suggestion - scroll

According to this link :
up/down arrow key issue with typeahead control (angular bootstrap UI)
i have added these line in my js:
.directive('shouldFocus', function(){
return {
restrict: 'A',
link: function(scope,element,attrs){
scope.$watch(attrs.shouldFocus,function(newVal,oldVal){
element[0].scrollIntoView(false);
});
}
};
})
while scrolling, we get some more disturb, scroll was not smoothy.
Before adding this code, scroll was normal and smoothy.
Please anyone help me?
Hi here is another code only for keyup event to adjust the scroll height while up/down keypress
you need to add a function only to keyup in the typeahead directive in angular ui
search directive('typeahead' in the angular ui js file
find fireRecalculating function before it paste the following function
function makeSureVisible(){
$timeout(function(){
$el = popUpEl.find('.active');
if($el.length>0)
{
var elTop, elBottom, nodeScrollTop, nodeHeight;
elTop = $el.position().top;
console.log(elTop);
elBottom = elTop + $el.outerHeight(true);
nodeScrollTop = popUpEl.scrollTop();
nodeHeight = popUpEl.height() + parseInt(popUpEl.css("paddingTop"), 10) + parseInt(popUpEl.css("paddingBottom"), 10);
if (elTop < 0) {
popUpEl.scrollTop(nodeScrollTop + elTop);
} else if (nodeHeight < elBottom) {
popUpEl.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
}
}
});
}
now find keyup function attached. call the above function
element.bind('keydown', function(evt) {
//typeahead is open and an "interesting" key was pressed
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
return;
}
// if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
resetMatches();
scope.$digest();
return;
}
evt.preventDefault();
if (evt.which === 40) {
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
makeSureVisible();
} else if (evt.which === 38) {
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
makeSureVisible();
} else if (evt.which === 13 || evt.which === 9) {
scope.$apply(function () {
scope.select(scope.activeIdx);
});
} else if (evt.which === 27) {
evt.stopPropagation();
resetMatches();
scope.$digest();
}
});
function makeSureVisible() {
$timeout(function () {
$el = popUpEl[0].querySelector('.active');
if ($el) {
var elTop, elBottom, nodeScrollTop, nodeHeight;
elTop = $el.offsetTop;
elBottom = elTop + $el.offsetHeight;
nodeScrollTop = popUpEl[0].scrollTop;
nodeHeight = popUpEl[0].offsetHeight - (parseInt(window.getComputedStyle(popUpEl[0], null).getPropertyValue('padding-top')) + parseInt(window.getComputedStyle(popUpEl[0], null).getPropertyValue('padding-bottom')));
if (elTop < nodeScrollTop) {
popUpEl[0].scrollTop = elTop;
} else if (nodeHeight < elBottom) {
popUpEl[0].scrollTop = nodeScrollTop + elBottom - nodeHeight;
}
}
});
}
I bumped into the same issue with latest version of angular bootstrap 0.14.3 as of 27/11/2015.
Comments:
I placed the function in the element.bind("keydown") since it didn't work where you suggested. (function wasn't in proper scope and was undefined)
The first "if" wasn't triggered for me so i changed the logic a bit. When user reaches the end the dropdown scrolls to top properly in my case.
Modified it to work for plain javascript.
Thank you for your solution!
I had previously resolved this issue using the "shouldFocus" directive but I had to make more tweaks to get this to work. Perhaps this version will work for you.
.directive('shouldFocus', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(attrs.shouldFocus, function (newVal, oldVal) {
if (newVal && element.prop("class").indexOf("active")) {
var par = element.parent("ul");
var cellHeight = element.children().innerHeight();
var maxHeight = par.height();
var startIndex = Math.floor(maxHeight / cellHeight);
if (scope.$index > startIndex) {
var scroll = (scope.$index - startIndex) * cellHeight;
par.scrollTop(scroll);
}
if (scope.$index === 0) {
par.scrollTop(0);
}
}
});
}
}
})
Here is the modified template for those who don't know where to add the directive:
angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("template/typeahead/typeahead-popup.html",
"<ul class=\"dropdown-menu\" ng-show=\"isOpen() && !moveInProgress\" ng-style=\"{top: position().top+'px', left: position().left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" +
" <li ng-repeat=\"match in matches track by $index\" should-focus=\"isActive($index)\" ng-class=\"{active: isActive($index) }\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{::match.id}}\">\n" +
" <div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" +
" </li>\n" +
"</ul>\n" +
"");
}]);

Time validation in angular js

I have a field which takes inputs of time something like 12:00 / 24:00
I want used to be able tot fill only digits and : in the field and on change validate it with proper time. I want to do it in angular way. How do i do it.
Here is the code i have written. This can be done in 1 directive also. This code works, But not better code.
<input type="text" ng-model-options="{allowInvalid: true}" ng-blur="validateHhMm(this)" ng-model='time' time-only/>
angular.module('abc').directive('timeOnly', function(){
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function (inputValue) {
var transformedInput = inputValue ? inputValue.replace(/[^\d:]/g,'') : null;
if (transformedInput!=inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
};
});
$scope.validateHhMm = function (inputField)
{
var errorMsg = "";
var regs = '';
// regular expression to match required time format
var re = /^(\d{1,2}):(\d{2})?$/;
var inputvalue = $scope.time;
if(inputvalue != '')
{
if(regs = inputvalue.match(re))
{
if(regs[4])
{
// 12-hour time format with am/pm
if(regs[1] < 1 || regs[1] > 12)
{
errorMsg = "Invalid value for hours: " + regs[1];
}
}
else
{
if(regs[1] > 23)
{
errorMsg = "Invalid value for hours: " + regs[1];
}
}
if(!errorMsg && regs[2] > 59)
{
errorMsg = "Invalid value for minutes: " + regs[2];
}
}
else
{
errorMsg = "Invalid time format: " + inputvalue;
}
}
if(errorMsg != "")
{
//console.log(errorMsg);
//field.focus();
$scope.time = '';
return false;
}
return true;
};
try this way
<input type="time" id="exampleInput" name="input" ng-model="example.value"
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
<span class="error" ng-show="myForm.input.$error.time">
Not a valid date!</span>
</div>
Here is the plunker
This is the example from here

how to wrap text in angularjs and save the position

I would like to wrap text with span tag and save the position.
I know how to do it with JS but i dont know how to do it with angularjs
Here is what i have done:
http://jsfiddle.net/ymeaL06j/1/
This function gives me the position of the text in the DIV
function getSelectionPosition() {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(document.getElementById("code"));
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
I take the start and end positions and insert them as an attribute in the span tag
After this i would like to save all the marked positions and load it later, i have a function that select text and then i can wrap it (i hope that there is a better solution)
function setSelection(savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(document.getElementById("code"), 0);
range.collapse(true);
var nodeStack = [containerEl], node, foundStart = false, stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
You can use the same code that you illustrated above and put it inside of your angularjs controller.
Refer to my plunker code; it is a simple angularjs version of your jsfiddle code.
For example, suppose that a snippet of the index.html looks like this:
<body ng-controller="MainCtrl">
<div id="code">This is <b>some text</b> bla bla bla</div>
<br />
<input type="button" value="Mark!" ng-click="markText()" />
<input type="button" value="Remove marks!" ng-click="removeMarks()" />
</body>
Then the example angularjs controller, MainCtrl, could look like this:
app.controller('MainCtrl', function($scope) {
var getSelectionPosition = function () {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(document.getElementById("code"));
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
}
$scope.markText = function() {
var currPosition = getSelectionPosition();
var selection = window.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = document.createElement("span");
span.className = "Mark";
span.setAttribute("PosStart", currPosition.start);
span.setAttribute("PosEnd", currPosition.end);
span.appendChild(selectedText);
selection.insertNode(span);
};
$scope.removeMarks = function() {
$(".Mark").each(function () {
$(this).contents().unwrap();
});
};
});
Notice that the MainCtrl is the angularjs controller for the body. The ng-click on the buttons reference the markText and removeMarks functions in the controller's scope. The logic in the functions are exactly the same as you referenced in your question (and jsfiddle).
None of your JS code changed other than moving the functions inside of the controller. Again, check out the plunker above to see the actual code working.

AngularJS : Two-way binding between a textarea and ng-repeat-ed inputs

I was going to ask this as a question, but I figured out a solution. So at this point, I'm looking for a critique of my solution.
I've got a static textarea, and an input with an ng-repeat directive.
As the user types a sentence into the textarea, a input is rendered for each word in the sentence.
Then if the user updates the text in any input, the corresponding word in the textarea sentence is updated (really the whole sentence is recreated).
Demo: http://plnkr.co/edit/bSjtOK?p=preview
Questions
Keeping in mind that I'm only 2 weeks into my AngularJS learning:
Did I write this in the "angular" way?
Is there something I could have done better?
Am I violating any no-nos?
Abbreviated Code
HTML
<textarea ng-model="sentence" ng-change="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-change="buildSentance(w)" />
JavaScript
function WordCtrl($scope, debounce) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
// this is called when the textarea is changed
// it splits up the textarea's text and updates $scope.words
$scope.parseSentence = function() {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i=0;i<words.length;i++) {
wordObjects.push({word: words[i]});
}
if ((words.length == 1) && (words[0] === '')) {
$scope.words = [];
} else {
$scope.words = wordObjects;
}
};
$scope.parseSentenceDebounced = debounce($scope.parseSentence, 1000, false);
$scope.buildSentance = function(w) {
var words = [];
for (var i=0;i<$scope.words.length;i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g,'') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
}
Interesting issue you are having. I put your code on my page and the first thing I noticed is that you cannot pass debounce in the controller method.
Next Problem I noticed is that you have an ng-change that changes the values on another box with ng-change. I changed the event to Keypress to stop the digest in a digest.
Here it is working in JSFiddle enter link description here
The code:
HTML
<body ng-app="portal">
<div ng-controller="WordCtrl">
<textarea ng-model="sentence" ng-keypress="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-keypress="buildSentance(w)" />
</div>
</body>
Javascript
angular.module("portal",[]).controller("WordCtrl",function($scope) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
$scope.parseSentence = function () {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i = 0; i < words.length; i++) {
wordObjects.push({ word: words[i] });
}
if ((words.length == 1) && (words[0] === ''))
{
$scope.words = [];
}
else
{
$scope.words = angular.copy(wordObjects);
}
}
$scope.buildSentance = function (w) {
var words = [];
for (var i = 0; i < $scope.words.length; i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g, '') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
});
Hope this solves your issue.

Resources