directive $formatters affect ngModel when writing - javascript

I have a problem to use $formatters.
My goal is to hide phone number, just leave the last 4 chars visible.
It's ok if you don't write anything in the input.
If you write something, the model is affected by the mask and I register the hidden phone in DB ...
Here's the directive I use:
.directive('tsHideField', function () {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, element, attributes, controller) {
var maskValue = function (value) {
if (!value) {
return "";
}
if (value.length <= 4) {
return value;
}
var valueHide = "";
if (value.indexOf('#') === -1) {
//leave last 4 chars
valueHide = value.toString().substring(0, value.length - 4).replace(/[\S]/g, "\u2022");
return valueHide + value.toString().substring(value.length - 4);
} else {
//Adresse email, on laisse après le # et on cache tout sauf les 4 dernières lettre avant
//'lambertjer#gmail.com'.substring(0,'lambertjer#gmail.com'.indexOf('#') - 4).replace(/[\S]/g, "\u2022") + 'lambertjer#gmail.com'.substring('lambertjer#gmail.com'.indexOf('#') - 4)
valueHide = value.toString().substring(0, value.indexOf('#') - 4).replace(/[\S]/g, "\u2022");
return valueHide + value.toString().substring(value.indexOf('#') - 4);
}
// replace all characters with the mask character
//return (value || "").replace(/[\S]/g, "\u2022");
}
/** SI ON VEUT EGALEMENT CACHER A L ECRIT:
*
* var createMaskedInputElement = function() {
if (! maskedInputElement || ! maskedInputElement.length) {
maskedInputElement = element.clone(true);
maskedInputElement.attr("type", "password"); // ensure the value is masked
maskedInputElement.removeAttr("name"); // ensure the password save prompt won't show
maskedInputElement.removeAttr("core.application.main.directive.mask"); // ensure an infinite loop of clones isn't created
maskedInputElement.bind("blur", function() {
element.removeClass("ng-hide");
maskedInputElement.remove();
maskedInputElement = null;
});
$compile(maskedInputElement)(scope);
element.after(maskedInputElement);
}
};
element.bind("focus", function() {
createMaskedInputElement();
element.addClass("ng-hide");
maskedInputElement[0].focus();
});
*/
controller.$formatters.push(function (value) {
return maskValue(value);
});
}
};
});
And for your facility, here's a fiddle with a little implementation:
http://jsfiddle.net/nqp4qtLk/2/
How to prevent model to be affected by the mask ??
EDIT: I adapt the answer of Gr3g to match to my requirements
see the updated fiddle: Updated fiddle

Please see my EDITED fiddles :
If you don't allow *'s to be deleted :
Fiddle
If you allow *'s to be deleted :
Punker
Note :
If you allow *'s to be deleted, you will see in the plunker I do not allow following :
- Deleting star(s) when number(s) are visible.
- Adding a number between 2 stars or at the first position.
Code has grown up so I can only show you partial code here.
Obviously, you needed the $parsers pipeline :
controller.$parsers.push(function(val){
//Modify your value
return modifiedValue || val;
});
Notice i added 2 functions in each pipeline so I can access a String in the function where I need to modify the value. I don't have to care (too much) about casts.
controller.$parsers.unshift(function(val){
return String(val);
});
You can probably make it faster, but be careful when refactoring to think about all possibilities to handle. Especially when *'s can be deleted.

I don't think you can, imagine i go between 2 points and delete one, how will you do ?
You should use 2 differents components : one to type each character, the other showing the phone number with only 4 last displayed.
The hardest possible way : handle all key event on the input yourself so you could even resolve what i said in the beginning of my post.

You can use $parsers.push to control value to be saved in the model.
var unmask = function(value) {
var original = scope.vm.phone.toString();
var last4 = value.substring(value.length-4);
var newstr = original.substring(0, original.length-4);
return (newstr+last4);
// you can have whatever logic you want, to manipulate the original value
}
controller.$parsers.push(function (value) {
return unmask(value);
// or do what ever you want.
});
Updated fiddle- http://jsfiddle.net/anh9y8d9/3/

Related

How do I access text of Select2 when there are multiple select2 instances?

I have the following code that works with the first selector and provides the result I am looking for, namely that results where the first to kth characters match rank above results where the characters merely exist in the option string.
(i.e. input string "ka" would rank kansas above alaska)
It fails when run in one of the other select2 instances on the site. Code as follows:
$(document).ready(() => {
userSettings.init()
$('.select2').select2({
sorter: function(data) {
return data.sort(function (a, b) {
// Declare levenshtein algorithm
var levenshtein = require('fast-levenshtein');
// Trim and set to lowercase on the inputs
a = a.text.trim().toLowerCase();
b = b.text.trim().toLowerCase();
/* This is the problem. I need a better way to retrieve the
input value in order to test it's fitness against my options. */
var input = $('.select2-search__field').val().trim().toLowerCase();
// Weigh by those that match the input in the beginning of the string first, and then list the rest that only contain the string next.
if (!isUndefined(input) && input.length > 0) {
if(input == a.substring(0, input.length)) {
return -1;
} else if (input == b.substring(0, input.length)) {
return 1;
} else {
// Input doesn't match the beginning of the string, still return strings with matching characters.
return levenshtein.get(a, input) - levenshtein.get(b, input);
}
}
});
// Function to ensure we are not operating on something that is undefined.
function isUndefined(value){
// Obtain `undefined` value that's guaranteed to not have been re-assigned.
var undefined = void(0);
return value === undefined;
}
}
})
});
As you can see, I need a better way to get at the input than the way I have declared var input.
**** EDIT ****
I think I have a solution.
I suspect that the sort was happening so fast, and so often, that it would just give up. We tested this by consoling out the workflow and we would erratically lose portions of it.
As a solution, I pulled the assignment out of actual sort function and then passed it in.
$(document).ready(() => {
userSettings.init()
$('.select2').select2({sorter: s2_sorter})
});
//Function to do sort on a Select2 Field.
let s2_sorter = function(data) {
/* Obtain the value out of the text field in order to test agaist the options during sort. */
var input = {d:$('.select2-container--open .select2-search__field').val().trim().toLowerCase()};
return data.sort(function (a, b) {
// Access the varible inside sort.
let i = input.d;
// Declare levenshtein algorithm
var levenshtein = require('fast-levenshtein');
// Trim and set to lowercase on the inputs
a = a.text.trim().toLowerCase();
b = b.text.trim().toLowerCase();
// Wieght by those that match character first, and then all with the character next.
if (!isUndefined(i) && i.length > 0) {
console.log('hassomethign')
if(i == a.substring(0, i.length)) {
return -1;
} else if (i == b.substring(0, i.length)) {
return 1;
} else {
// Input doesn't match the begining of the string, still return strings with matching characters.
return levenshtein.get(a, i) - levenshtein.get(b, i);
}
}
});
function isUndefined(value){
// Obtain `undefined` value that's guaranteed to not have been re-assigned.
var undefined = void(0);
return value === undefined;
}
};

Angularjs create input mask

I'm trying to create a directive to create custom mask for my input. I know there are other libraries I could use, but sometimes I need a custom input based on the company needs, (e.g. "OS.012-08765"), so I'd rather create my own directive.
So far I was able to format the number on the pattern I need, but not on the input, only on the model. For this example I'll be using a money input, because it's simpler to work with (I think). This is the code I'm using:
function myMoney() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModelCtrl) {
ngModelCtrl.$formatters.push( formatInput );
ngModelCtrl.$render = renderViewValue;
ngModelCtrl.$parsers.push( parseOutput );
function formatInput( value ) {
value = formatNumber(value);
return value;
}
function parseOutput( value ) {
value = formatNumber(value);
return value;
}
function renderViewValue() {
element.html( ngModelCtrl.$viewValue );
}
function formatNumber(value) {
if(!value) {
value = '000';
}
var checkValue = value.replace(/[^0-9\.]/g, '');
if(checkValue != value) {
value = checkValue;
}
value = value.toString();
if(value.length < 3) {
value = ('000'+value).slice(-3);
}
var
firstChar = value.slice(0, value.length-2),
lastChar = value.slice(-2),
firstChar = Number(firstChar).toLocaleString(),
finalChar = '$'+firstChar+','+lastChar;
return finalChar;
}
}
}
}
And this is a plunkr with the code: http://plnkr.co/edit/UjZkPFL0V4FRrDUL9jAJ?p=preview
The main problem is where the output is, if you start typing on the input, the value doesn't have the mask, is just numbers. But the model has the proper mask.
So, based on this, I have 2 main issues:
First: I want to invert these results. I want to type in the textbox and have the mask on the textbox while the model is just plain number (without the mask).
Second: If I create a button to update the value on the model, it doesn't get formatted within the mask, it stays plain text.
How can I solve these problems?
try to use ui mask, https://htmlpreview.github.io/?https://github.com/angular-ui/ui-mask/master/demo/index.html, enter AA.999-99999 under Mask Definition field to match your pattern.
<input type="text"
ng-model="serviceOrderNumber"
ui-mask="AA.999-99999"
ui-mask-placeholder
ui-mask-placeholder-char="_"/>

Replacing angular element html within a directive's link

This is my first Angular Directive.
I am trying to do a simple highlight on a html content based on the search terms used to find that content.
The problem is, that is working for the first term, but not for more. I want to all words get highlighted but I am doing something wrong when I replace the HTML content.
This is what the directive tries to do:
1.
The directive should highlight one or more words. For example.
If the search terms are "document legal" it should highlight both of them, even if they are not on this order.
So, a text like "legal something document" should get both highlighted, "legal" and "document".
2.
If the word is less than 3 characters is not going to get highlighted.
3.
If the word is not found, try removing the last character from it until its length is less than 3. You may search for "dimensions" and the search engine may return a text containing "dimension" or even "dime".
Just in case, the app is an Ionic App.
This is my code.
The angular directive:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
link: function($scope, element, attrs) {
$scope.highlightTerm = function(term) {
var html = element.html();
var highlighted = html.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
if (highlighted == null) {
return false;
}
// #see
// I think that the problem is here, it works the
// first time, but the second time it gets here
// the following exception is throwed
// "Cannot read property 'replaceChild' of null"
element.replaceWith(highlighted);
return html != highlighted;
};
var searchTerms = $scope.searchTerms;
if (searchTerms != undefined && searchTerms.length < 3) {
return;
}
var terms = searchTerms.split(' ');
// Try to highlight each term unless the word
// is less than 3 characters
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
// // Try to highlight unless the word is less than 3 chars
while (term.length > 2) {
// If it got highlighted skip to next term
// else remove a character from the term and try again
if ($scope.highlightTerm(term)) {
break;
}
term = term.substring(0, term.length - 1);
}
}
}
};
});
You can see some weird things. Like using $scope.highlightTerm instead of passing the highlightTerm var to the directive. I couldn't get it work.
How can I change the HTML of the element correctly?
This is the template that is using the directive:
<div ng-include src="tplName" highlight-search-terms></div>
I wish to do something like that but I couldn't get it working:
<div ng-include src="tplName" highlight-search-terms="something to highlight"></div>
Here is a Plunker:
http://plnkr.co/edit/BUDzFaTnxTdKqK5JfH0U?p=preview
I think your code is working, but the issue was that you are trying to replace the whole div that is using the directive. So what you can do is just replace element.replaceWith(highlighted); with element.html(highlighted); and it will work.
I wish to do something like that but I couldn't get it working: <div
ng-include src="tplName" highlight-search-terms="something to
highlight"></div>
You already there, just use attrs in the link function like so:
var terms = attrs.highlightSearchTerms;, and you will get what you passed in highlight-search-terms="something to highlight"
This should work for you, with using of 'compile' function:
angular.module('starter.directives', [])
.directive('highlightSearchTerms', function($compile) {
return {
restrict: 'A',
scope: true,
compile: function(elem, attrs) {
// your code
elem[0].innerHTML = '<span class="highlightedText">$&</span>';
// your code
}
};
});
Documentation also could help.
Even tough punov's solution works, I think you shouldn't trigger multiple re-compiles for a single "line". I would suggest storing the html in a variable and recompile after every term was replaced.
Here is a working example - but it needs some polishing.
http://plnkr.co/edit/3zA54A0F2gmVhCComXAb?p=preview
link: function($scope, element, attrs) {
var searchTerms = $scope.searchTerms;
var terms = searchTerms.split(' ');
$scope.highlightedHTML = element.html();
if (searchTerms !== undefined && searchTerms.length < 3) {
return;
}
$scope.highlightTerm = function(term) {
console.log("html - ", term, html);
var highlighted = $scope.highlightedHTML.replace(new RegExp(term, 'gi'),
'<span class="highlightedText">$&</span>');
//element.replaceWith(highlighted);
return highlighted;
};
function highlight(terms, compile) {
// Try to highlight each term unless the word
// is less than 3 characters
// if the term is not highlighted remove one character
// from it and try again
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
while (term.length > 2) {
var current = $scope.highlightedHTML;
$scope.highlightedHTML = $scope.highlightTerm(term);
if (current !== $scope.highlightedHTML) {
break;
}
term = term.substring(0, term.length - 1);
}
}
compile();
}
highlight(terms, function() {
element.replaceWith( $scope.highlightedHTML);
});
}

Knockout Extension Issue - Better Solution?

I'm new to knockout, and still learning how best to work with it. I have a few input fields in an app which are tied to a bunch of calculations that update in real time. The fields on their own work great, and all is fine...
EXCEPT, I need to format the input as the user enters it, for display only (the raw data must be retained for the calculations, but 3 should appear as 3% or in another field 3000000 should appear as 3,000,000 etc.). I have this somewhat working, but I think there's a major flaw with my solution as the result is consistently buggy and it's possible to break the input field entirely.
So, an example of one of the input fields, which ties to another field to always equal 100%:
<input id='sm' data-bind='textInput: s_smixe' readonly='true'>
Is bound to:
self.s_smixebase = ko.observable(30);
self.s_smixe = ko.pureComputed({
read: function(){
return this.s_smixebase();
},
write: function(value){
if (parseFloat(value)<100) {
var otherValue = 100 - parseFloat(value);
this.s_smixebase(value);
this.s_rmixebase(otherValue);
} else {
value = 100;
this.s_smixebase(value);
this.s_rmixebase(0);
}
},
owner: this
}).extend({percent:{}});
self.s_smixeraw = self.s_smixe.raw;
Which is then extended by:
ko.extenders.percent = function(target) {
var raw = ko.observable();
var result = ko.computed({
read: function() {
var value = target();
if (value.toString().indexOf('%')===-1){
raw(parseFloat(value));
value = value + '%';
return value;
} else {
value = value.replace('%','');
raw(parseFloat(value));
value = value + '%';
return value;
}
},
write: target
}).extend({notify:'always'});
result.raw = raw;
return result;
};
So, what happens here, is that the first character input by the user formats correctly, the second character input by the user disappears, and the third joins the first and formats correctly. This happens the same if the field is computed or a regular observable, and the computed code is working fine without the extension applied. So to input 77% you would have to type 7 - X - 7 (where X can be any value since it gets lost to the process somewhere).
It should also be noted that I am using a virtual javascript numeric keyboard in this app so I am adding values via javascript (though this has not affected any of the other functionality, so I'm not sure why it would here).
Can anyone offer suggestions on what I'm doing wrong? What am I missing that is causing the input to be so buggy? I'm really determined not to ditch this notion of real-time input formatting as it makes for much cleaner presentation, but I if I have to I'll just format on blur.
Thanks in advance for any suggestions.
Because it's tricky to position the cursor properly when the formatting function replaces what you're typing as you type, I'd recommend having a field that has two modes: one where you're typing in it, and the other where it's displaying the formatted value. Which displays depends on cursor focus.
<div data-bind="with:pctInput">
<label>Value</label>
<input class="activeInput" data-bind='textInput: base, event:{blur:toggle}, visible:editing, hasFocus:editing' />
<input data-bind='textInput: formatted, event:{focus:toggle}, visible:!editing()' readonly='true' />
</div>
A working example is here:
http://jsfiddle.net/q473mu4w/1/
So, for anyone who comes across this later, I ended up using a modified version of #RoyJ 's solution from the thread mentioned in the initial comments. I do need to come up with a way to make this scale if I'm ever going to use it in larger projects, but it's sufficient for something with a small number of inputs. Also, in my case there are many formatted fields calculating their values based on the inputs, hence the multPercent and multNumber computed values. I wanted to ensure that all the inputs were carrying over properly to calculations. Here's a sample of the code with a working jsfiddle below:
<input data-bind="textInput:textPercent" />
<div data-bind="text:multPercent"></div>
<input data-bind="textInput:textNumber" />
<div data-bind="text:multNumber"></div>
and the accompanying javascript:
function dataBindings() {
var self = this;
self.percent = function(str){
var splice = str.toString().replace('%','');
splice = splice + '%';
return splice;
};
self.number = function(numStr){
var formatted;
if (Number(numStr) % 1) {
var integer = numStr.toString().replace(/\.\d+/g,'');
var decimal = numStr.toString().replace(/\d+\./g,'');
integer = integer.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); //add comma formatting
formatted = integer + '.' + decimal;
console.log('formatted = '+formatted);
return formatted;
} else {
formatted = numStr.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
return formatted;
}
};
self.displayPercent = ko.observable('5%');
self.rawPercent = ko.observable(5);
self.formattedPercent = ko.computed({
read: function() {
return self.displayPercent();
},
write: function(newValue) {
if (newValue==='') {
newValue = 0;
self.rawPercent(0);
var f = self.percent(newValue);
self.displayPercent(f);
} else {
if (newValue.charAt(0)==='0') {
newValue = newValue.slice(1);
}
self.rawPercent(parseFloat(newValue.toString().replace('%','')));
var f = self.percent(newValue);
self.displayPercent(f);
}
}
});
self.displayNumber = ko.observable('3,000');
self.rawNumber = ko.observable(3000);
self.formattedNumber = ko.computed({
read: function(){
return self.displayNumber();
},
write: function(newValue) {
if (newValue==='') {
newValue = 0;
self.rawNumber(0);
self.displayNumber('0');
} else {
if (newValue.charAt(0)==='0') {
newValue = newValue.slice(1);
}
newValue = newValue.replace(/(,)+/g,'');
self.rawNumber(parseFloat(newValue));
var n = self.number(newValue);
self.displayNumber(n);
}
}
});
self.multPercent = ko.computed(function(){
return self.percent(self.rawPercent() * self.rawPercent());
});
self.multNumber = ko.computed(function(){
return self.number(self.rawNumber() * self.rawNumber());
});
return {
textPercent: self.formattedPercent,
multPercent: self.multPercent,
textNumber: self.formattedNumber,
multNumber: self.multNumber
};
}
ko.applyBindings(new dataBindings());
http://jsfiddle.net/jschevling/mwbzp55t/

Blocking the first number in a text field

I have a text input binded to a variable that contains a 5 digits number. How can I block the first digit so only the other 4 are editable?
Currently I have this: ng-model="my_var" ng-pattern="/^\d{5}$/"
Please note that the value is two-way binded, which means I'm showing it and the user can edit /save it.
You could use a custom directive that would work in conjunction with ngModel.
It could use the parser/formatter to modify the value read/printed.
Read more about it here: http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController ($formatters and $parsers)
Here is a working solution (you will need probably tweak it a little bit for your particular needs):
app.directive("filterInput", function () {
return {
require: 'ngModel', // controller to ng-model needed
restrict: 'A',
link: function (scope, element, attrs, modelCtrl) {
var unchanged = attrs.filterInput;
var regex = /^\d{0,5}$/; // the RegExp (in more generic solution could be passed from outside)
modelCtrl.$parsers.push(function (inputValue) { // adding new $parser
if(inputValue[0] != unchanged){ // if the first digit is different from what it should be set value as the first digit
modelCtrl.$setViewValue(unchanged);
modelCtrl.$render();
return unchanged;
}
if(inputValue.length > 5){ // trim the input if it is longer than five -- it is not generic at all
var rv = inputValue.substring(0,5);
modelCtrl.$setViewValue(rv);
modelCtrl.$render();
return rv;
}
var transformedInput = regex.exec(inputValue);
if (transformedInput != null) { // check if pattern exists
transformedInput = transformedInput[0];
}
else {
transformedInput = unchanged; // if not set the value to the not changeable part
}
if (transformedInput != inputValue) { // re-render if value changed
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
}
});
And how to use it:
<p>var: {{my_var}}</p>
<input type="text" ng-model="my_var" filter-input="1"/>
PLNKR
Btw: it will allow to pass only digits :-)
Perhaps not the best solution but it works:
HTML
<input ng-model="my_var" maxlength="5">
Controller
$scope.my_var = '1';
$scope.$watch('my_var', function(){
if($scope.my_var.length != 1){
if($scope.my_var.length == 0){
$scope.my_var = '1';
}
}
});

Categories