Replacing angular element html within a directive's link - javascript

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

Related

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="_"/>

directive $formatters affect ngModel when writing

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/

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

AngularJS asynchronous directive compilation

I'm writing an angularjs app (with a node/express backend). I have in my scope an object that gets populated (and repopulated) asynchronously via a service.
The contents of the object is a series of "question" objects, which for simplicity have a "text" and a "type" attribute.
What I'm trying to achieve is to be able to have an angular directive per type, and for these to render properly. So for example if the server returns [{type:'booleanquestion',text:'Is the sky blue?'}] I would create an element , and then the booleanquestion directive will kick in and render it appropriately.
So far I have defined a "questionset" directive which works well, watching the questions collection and compiling the required stuff so the directives display correclt.y
Html
<div questionset="questions"/>
app.js
...
scope.questions = questions; // e.g. [{text: 'some text',type: 'boolean'}]
directives.js
angular.module('myApp').directive('questionset',function($compile){
return {
transclude: 'true',
compile: function(element,attr,linker){
return function($scope,$element,$attr){
var elements = [],
parent = $element.parent();
$scope.$watchCollection('questions',function(questions){
var block, i, childScope;
if(elements.length > 0)
{
for( i = 0; i < elements.length ; i++)
{
elements[i].el.remove();
elements[i].scope.$destroy();
}
elements = [];
}
if(!questions) return;
for(i = 0; i < questions.length; i++)
{
childScope = $scope.$new();
childScope['question'] = questions[i];
var html = '<div class="'+questions[i].type+'"/>';
var e = angular.element(html);
block = {};
block.el = e;
block.scope = childScope;
$compile(e)(childScope);
element.append(e);
elements.push(block);
}
});
}
}
}
});
// For example, one type might be "boolean"
angular.module('myApp').directive('boolean',function($compile){
return {
restrict: 'AC',
template: '<div class="question">{{question.text}}</div>',
replace: true,
link: function(scope,elem,attrs,ctrl){
…….
// for some reason, $compile will not be defined here?
}
};
});
Whilst this is working OK, I have 2 questions
1). Is this the "right" angular way to do this? This is my first angular project and it seems I've jumped in rather at the deep end (or that's how it feels anyway)
2). My next goal is for the question.text to be able to contain HTML and for that to be "compiled". For example the text might be
"Is the <strong>sky</strong> blue?"
I'm not sure how to make this work - as my comment in the code suggest, for some reason $compile is not being injected into my boolean directive. Perhaps this is because I've manually created that child scope for it? Is it right I'm trying to $compile again the contents of the element? I feel like this last bit is probably very simple, but I'm not seeing it.
1) I don't know. In my opinion, the overall approach seems very nice; just need to polish it, as we'll see below.
2) Maybe $compile is not available at the nested function because it is not used in the parent level. Try referencing $compile in the parent function to see if this is really the reason:
angular.module('achiive-vision').directive('boolean',function($compile){
var justTesting = $compile;
return {
restrict: 'AC',
template: '<div class="question">{{question.text}}</div>',
replace: true,
link: function(scope,elem,attrs,ctrl){
…….
// check if $compile is defined here now
}
};
});
I would simplify things by changing the questionset elements html to:
var html = '<div class="question ' + questions[i].type + '">'
+ questions[i].text + '</div>';
This way, you don't need to compile again, and you will already have HTML support.
Also, it is a bit strange to have
var html = '<div class="'+questions[i].type+'"/>';
above, and then you will REPLACE it by a similar markup:
template: '<div class="question">{{question.text}}</div>',
...and then you want to COMPILE this again...
In my opinion, you don't need replace: true and you don't need the template at the question type directives. Implement the behaviour and anything else on them, but let the parent questionset include the question text and compile it - everything is ready for this.
Also, a detail: I wouldn't user var html and var e inside a loop. I would add them to the list above:
var block, i, childScope, html, e;
(Also, I would avoid naming variables as "i" and "e"...)
These are my comments regarding your code. As I said, I think the approach is very nice, not to say "advanced" for someone who is beginning with AngularJS. I myself am using it for less than two months, only.

Search For Words, Replace With Links

I have an array like this
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}
];
I want it to search the page for word and replace it with link. Is there an efficient way of doing this? It seems it may be CPU hungry.
Sorry should have explained more...
It would search each element with the class .message for instance. Then find all of the words within that class and replace it with link.
There would also be a few hundred within this array
A good strategy is:
1) Build an object whose keys are the phrases to replace and whose values are the links to replace them with.
2) While doing that, construct a regular expression that can match any of the keys, then
3) Use that regex to globally replace.
Rough example:
var replacementDict = {
'foo': 'http://www.foo.com/',
'bar': 'http://www.bar.net/'
};
var theRegex = /\b(foo|bar)\b/g;
theText.replace(theRegex, function(s, theWord) {
return "<a href='" + replacementDict[theWord] + "'>" + theWord + "</a>";
});
Given some content like:
<div class="message">Somethsg1</div>
<div class="message">Something</div>
<div class="message">Ssething</div>
<div class="message">Something Else</div>
<div class="message">Something da</div>
<div class="message">Somethin2g</div>
You can use something like:
//your array
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}
];
//iterate the array
$.each(words,
function() {
//find an element with class "message" that contains "word" (from array)
$('.message:contains("' + this.word + '")')
//substitute html with a nice anchor tag
.html('' + this.link + '');
}
);
This solution has one immediate problem though (showed in the example too). If you search for example for Something and you find Something beautiful, the "contains" will be match.
If you want a strict selection, you have to do:
//for each array element
$.each(words,
function() {
//store it ("this" is gonna become the dom element in the next function)
var search = this;
$('.message').each(
function() {
//if it's exactly the same
if ($(this).text() === search.word) {
//do your magic tricks
$(this).html('' + search.link + '');
}
}
);
}
);
It's your choice whether to iterate all array elements first then all the doms, or the other way around. It's also depends on which kind of "words" you are gonna search (See the two example for the "why").
BIG WARNING: if the array contains user-defined content, you have to sanitize it before injiecting it to the elements' html!
It would be possible to do it with something like:
$('*:contains("string to find")');
the problem with this approach is that "*" will return all elements that contain the string, including HTML, BODY, etc... and after that you still need to find the string inside the text node of each element, so it may be easier to just go and check every text node...
I'd suggest you take a look at the highlight plugin that already does something very similar to what you want (instead of linking, it highlights any text on a page), but from the source code it seems pretty easy to change it.
If you want to wrap in 'a' tag un-comment code and comment call above.
Try this:
var words = [
{
word: 'Something',
link: 'http://www.something.com'
},
{
word: 'Something Else',
link: 'http://www.something.com/else'
}];
var changeWordsWithLink = function (words) {
if(document.getElementById && document.getElementsByTagName) {
var messages = document.getElementById('message');
if(messages) {
for(i = 0; i < messages.length; i++){
for (j = 0; j < words.length; j++) {
if(words[j].word == messages[i].innerHTML) {
messages[i].innerHTML = words[j].link;
//messages[i].innerHTML = wrapInATag(words[j].link, words[j].word);
}
}
}
}
}
}
var wrapInATag = function(link, word) {
return '' + word + '';
}

Categories