How to replace multiple keywords in a string with a component - javascript

To start off, I'm primarily an AngularJS developer and recently switched to React, and I decided to convert an angular webapp I had previously developed to a react app. Im having a bit of an issue with a component ExpressiveText that searches through a string for a match to a property on a list objects and inserts a component TriggerModal in its place that when clicked triggers a modal with more detailed information. So the properties passed into ExpressiveTest are: text, tags, and tagsProperty.
text is a string (i.e. "My search string")
tags is an array of objects (i.e. [{id: 1, name: 'my', data: {...}}, {id: 2, name: 'string', data: {...}}]
tagsProperty is the name of the property to search for as a "tag" (i.e. name)
I followed along with this issue to try and formulate an idea of how to approach this. The reason I mention that I am coming from angular is because the component I had previously created simply used something like text.replace(regex, match => <trigger-modal data={tags[i]} />) and then used angulars $compile function to render components in the text. This does not seem to be possible using react. This is what I have tried inside of my ExpressiveText component:
class ExpressiveTextComponent extends React.Component {
constructor (props) {
super(props);
this.filterText = this.filterText.bind(this);
}
filterText () {
let text = this.props.text;
this.props.tags.map(tag => {
const regex = new RegExp(`(${tag[this.props.tagsProperty]})`, 'gi');
let temp = text.split(regex);
for(let i = 1; i < temp.length; i+=2){
temp[i] = <TriggerModal data={tag} label={tag[this.props.tagsProperty]} />;
}
text = temp;
});
return text;
}
render () {
return (
<div className={this.props.className}>{this.filterText()}</div>
);
}
}
This works for the first tag. The issue with it is that once it goes to map on the second tag, text is then an array. I tried adding in a conditional to check if text is an array, but then the issue becomes that the text array becomes nested and doesnt work on the next iteration. Im having a really hard time wrapping my mind around how to handle this. I have also tried dangerouslySetInnerHTML using text.replace(...) but that doesn't work either and just renders [object Object] in place of the component. Any help or advice is much appreciated, I have to say this is probably the only major issue I have come across since my switch to React, otherwise its been very straightforward.
Edit: Since I had a question asking for expected output with a given input and more clarification, what I am looking for is a component that is given this input:
<ExpressiveText text="my text" tags={{id: 1, name: 'text'}} tagsProperty="name" />
would render
<div>my <TriggerModal label="text" data={...} /></div>
with a functional TriggerModal component.

If I am correct in my understanding of what you're trying to accomplish, this is one way to do this it. My apologies if I misunderstood your question. Also, this is pseudocode and I'll try and fill it in with real code in a bit. Sorry if this is difficult to understand, let me know and I will try to clarify
filterText () {
let text = [this.props.text];
for (let item in this.props.tags) {
//item will be something like {id: 1, name: 'text'}
let searchString = new RegExp(item.name, 'gi');
//loop through text array and see if any item matches search string regex.
while (text.some(val => val.test(searchString)) {
//if we are here, at least one item matches the regexp
//loop thru text array, and split any string by searchString, and insert <TriggerModal> in their place
for (let i = text.length-1; i >=0; i--) {
//if text[i] is string and it matches regexp, then replace with nothing
text[i].replace(searchString, "")
//insert <trigger modal>
text.splice(i, 0, <TriggerModal ... />)
}
//end of while loop - test again to see if search string still exists in test array
}
}
return text;
}

Looks like I found a solution.
filterText () {
let text = this.props.text.split(' '),
replaceIndexes = [];
if(this.props.tags.length > 0) {
this.props.tags.map(tag => {
const regex = new RegExp('(' + tag[this.props.tagsProperty] + ')', 'gi');
for(let i = 0; i < text.length; i++){
if(text[i].match(regex)){
/**
* Pretty simple if its a one-word tag, search for the word and replace.
* could potentially cause some mis-matched tags but the words
* in my usecase are pretty specific, unlikely to be used in
* normal dialogue.
*/
text[i] = <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />;
}else{
// for tags with spaces, split them up.
let tempTag = tag[this.props.tagsProperty].split(' ');
// check for length
if(tempTag.length > 1) {
// we will be replacing at least 1 item in the array
let replaceCount = 0,
startIndex = null;
// If the first word of tempTag matches the current index, loop through the rest of the tempTag and check to see if the next words in the text array match
if(tempTag[0].toLowerCase() === text[i].toLowerCase()){
startIndex = i;
replaceCount += 1;
// loop through temp array
for (let j = 0; j < tempTag.length; j++) {
if(tempTag[j].toLowerCase() === text[i+j].toLowerCase()){
replaceCount += 1;
}
}
// Push data into replaceIndexes array to process later to prevent errors with adjusting the indexes of the text object while looping
replaceIndexes.push({
startIndex: startIndex,
replaceCount: replaceCount,
element: <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />
});
}
}
}
}
});
}
// Loop through each replace index object
replaceIndexes.forEach((rep, index) => {
text.splice(rep.startIndex - index, rep.replaceCount, [rep.element, ', ']);
});
// Since we stripped out spaces, we need to put them back in the places that need them.
return text.map(item => {
if(typeof item === "string"){
return item + ' ';
}
return item;
});
}
Edit: This is actually pretty buggy. I ended up ditching my own solution in favor of this package

Related

How to replace overlapping strings in Javascript without destroying the HTML structure

I have a string and an array of N items:
<div>
sometimes the fox can fly really high
</div>
const arr = ['the fox can', 'fox can fly', 'really high']`
I want to find a way to replace the text inside the div with HTML to highlight those specific phrases inside the array without breaking the HTML. This can be problematic because I can't do a simple loop and replace because then other words will not match after a replacement because the highlight span would break something like indexOf or includes on the innerHTML, sure I can use innerText to read the text but it doesn't provide anything that makes it so I can add the "next" span without breaking the original HTML highlights. Ideally, I also want to be able to customize the class name depending on the word I use rather than just a generic highlight class too.
The outcome should be
<div>
sometimes
<span class="highlight-1">the <span class="highlight-2">fox can</span></span><span class="highlight-2"> fly</span> <span class="highlight-3">really high</span>
</div>
What have I tried?
I've really thought about this and cannot find any resources online that help with this scenario and the main, Currently, I also need extra values such as charStart and charEnd of the word, I don't like this solution because it depends on using the DOMParser() API and it feels really hacky, definitely isn't performant and I just get a "vibe" that I shouldn't be doing this method and there must be better solutions, I am reaching out to SO for ideas on how I can accomplish this challenge.
let text = `<p id="content">${content}</p>`
let parser = new DOMParser().parseFromString(text, "text/html")
for (const str of strings) {
const content = parser.querySelector("#content")
let descLength = 0
for (const node of content.childNodes) {
const text = node.textContent
let newTextContent = ""
for (const letter in text) {
let newText = text[letter]
if (descLength === str.charStart) {
newText = `<em class="highlight ${str.type}" data-id="${str.id}">${text[letter]}`
} else if (descLength === str.charEnd) {
newText = `${text[letter]}</em>`
}
newTextContent += newText
descLength++
}
node.textContent = newTextContent
}
// Replace the < with `<` and replace > with `>` to construct the HTML as text inside lastHtml
const lastHtml = parser
.querySelector("#content")
.outerHTML.split("<")
.join("<")
.split(">")
.join(">")
// Redefine the parser variable with the updated HTML and let it automatically correct the element structure
parser = new DOMParser().parseFromString(lastHtml, "text/html")
/**
* Replace the placeholder `<em>` element with the span elements to prevent future issues. We need the HTML
* to be invalid for it to be correctly fixed by DOMParser, otherwise the HTML would be valid and *not* render how we'd like it to
* Invalid => `<span>test <em>title </span>here</em>
* Invalid (converted) => `<span>test <em>title </em></span><em>here</em>
* Valid => `<span>test <span>title </span>here</span>
*/
parser.querySelector("#content").innerHTML = parser
.querySelector("#content")
.innerHTML.replaceAll("<em ", "<span ")
.replaceAll("</em>", "</span>")
}
I'll go over your example just to give an idea. Below code is not a clean function, please adjust it according to your needs.
const str = "sometimes the fox can fly really high";
const arr = ['the fox can', 'fox can fly', 'really high'];
// First, find the indices of start and end positions for your substrings.
// Call them event points and push them to an array.
eventPoints = [];
arr.forEach((a, i) => {
let index = strLower.indexOf(a)
while (index !== -1) {
let tagClass = `highlight-${i}`
eventPoints.push({ pos: index, className: tagClass, eventType: "start" })
eventPoints.push({ pos: index + a.length, className: tagClass, eventType: "end" })
index = strLower.indexOf(a, index + 1)
}
return
});
// Sort the event points based on the position properties
eventPoints.sort((a, b) => a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : 0);
// Init the final string, a stack and an index to keep track of the current position on the full string
let result = "";
let stack = [];
let index = 0;
// Loop over eventPoints
eventPoints.forEach(e => {
// concat the substring between index and e.pos to the result
result += str.substring(index, e.pos);
if (e.eventType === "start") {
// when there is a start event, open a span
result += `<span class="${e.className}">`;
// keep track of which span is opened
stack.push(e.className);
}
else {
// when there is an end event, close tags opened after this one, keep track of them, reopen them afterwards
let tmpStack = [];
while (stack.length > 0) {
result += "</span>";
let top = stack.pop();
if (top === e.className) {
break;
}
tmpStack.push(top);
}
while (tmpStack.length > 0) {
let tmp = tmpStack.pop();
result += `<span class="${tmp}">`;
stack.push(tmp);
}
}
index = e.pos;
});
result += str.substring(index, str.length)
console.log(result);

How to remove the first index of an array based on input value?

I apologize in advance if i'm not conveying things properly.
I'm trying to take an array, split the strings into characters, and remove the first index if it matches my input value, and continue linearly.
I'm a little lost on how I should be thinking about solving this problem.
So far I have gotten to here,
showCurrentValue = (event) => {
const value = event.target.value;
document.getElementById("textV").innerText = value;
let arrIndex = newArr[0].split('');
for (let i = 0; i < arrIndex.length; i++) {
if (value === arrIndex[0]) {
console.log("Match");
arrIndex.shift()
} else {
console.log("Err")
}
}
}
With or without the loop it still behaves the same, it DOES remove the first index, but fails to continue, and logs "Err" for all the rest of the characters in the string. It won't match the next character. In my head i'm thinking if I just target the 0 index, and the array will update as each character is removed(?).
Hope someone can shine some light on this, Thanks!!
Basically, i'm trying to build one of those Typing speed test applications.
So I have my array of randomized words rendered to the DOM with an input field below it.
If, the input value matches the first character, I want to manipulate it based on if true or false. Change it's color, remove it from the DOM, etc.
So, my issue currently is getting the 2nd character of my input to compare to the next current character index.
Maybe my whole approach is wrong(?)
The first time you hit a situation where your value !== arrIndex[0], you keep viewing the same element (arrIndex[0]) and not iterating to the next element.
#wlh's comment is pretty close to the solution I think you're looking for:
const filtered = arrIndex.filter(char => char !== value);
filtered will then have all the elements of arrIndex but remove any and all elements that matched value.
This is just one possible solution. You continue moving thru arrIndex with out changing its length, checking each case, and adding the cases which don't match your value to a new array. Once you have finished moving thru the arrIndex then you assign arrIndex to the updatedIndex;
showCurrentValue = (event) => {
const value = event.target.value;
let updatedIndex = [];
const arrIndex = newArr[0].split('');
for (let i = 0; i < arrIndex.length; i++) {
if (value !== arrIndex[i]) {
console.log("Err");
updatedIndex.push(arrIndex[i]);
} else {
console.log("Match")
}
}
arrIndex = updatedIndex.join('');
}
Another solution could be
showCurrentValue = (event) => {
const value = event.target.value;
let arrIndex = newArr[0].split('').filter((char) => char !== value);
return arrIndex;
}
.filter will return a new array - it will loop thru each char and return ones which don't match your value

ReactJS is it possible to render a string?

I have a code that returns a string, but since it's on render it needs to be accompanied by an HTML tag, in this case span.
I want to use this code in multiple places including placeholders and labels, but with span, I believe that's impossible.
Here's the code in question, would appreciate some ideas on how to fix.
let LocalizedString = React.createClass({
render: function() {
if (!this.getKey(loadLang, this.props.xlKey)) {
return <span>{this.props.xlKey + ' untranslated in ' + userLang + ' JSON'}</span>;
} else {
return <span>{this.getKey(loadLang, this.props.xlKey)}</span>;
}
},
getKey: function(obj, str) {
str = str.replace(/\[(\w+)\]/g, '.$1'); // let's convert indexes to properties
str = str.replace(/^\./, ''); // gets rid of leading dot
let a = str.split('.');
for (let i = 0, n = a.length; i < n; i++) {
let key = a[i];
if (key in obj) {
obj = obj[key];
} else {
return null;
}
}
return obj;
},
});
module.exports = LocalizedString;
In another file that calls LocalizedString, I have this, but makes placeholder return undefined...
<TextField alert={alerts.username} onChange={this._onChangeUsername} placeholder={<LocalizedString xlKey='pages.signin.general.username'/>} ref="username" value={this.props.username}/>
Bottom line is pretty much, can I make render return just a string without any tags or make placeholder accept and discard the span tag?
Regards.
You must have to use a function instead of a React Component as per the requirement.
React Component's render method must be bound to return single wrapped markup or essentially a Virtual DOM.

Recursively constructing a JavaScript array of all possible combinations while respecting order

I am having difficulty getting the following concept in to code:
Let's say we are given the following array:
[
'h1,h2',
'span,style'
]
From this I wish to get the following output:
[
'h1 span',
'h1 style',
'h2 span',
'h2 style
]
So that we have an array of strings containing all combinations of the original array, which also respects order (so span h1 and style h2 are not valid).
To describe verbose: I have a single level array of strings which are effectively comma separated values. I wish to iterate over this array and split these strings by their comma in to a new array, and for each index in this new array build a new string which contains all the other split values from subsequent indexes in the original array.
I am having difficulty trying to program this in JavaScript. I understand that I will need some recursion, but I'm confused about how to do it. After trying various different and failing methods, I currently have this:
function mergeTagSegments(arr, i, s) {
i = i || 0;
s = s || '';
if(!arr[i]) { return s; }
var spl = arr[i].split(',');
for(var j in spl) {
s += spl[j] + mergeTagSegments(arr, i+1, s);
}
return s;
}
This also fails to work as intended.
I feel a little embarrassed that I am unable to complete what I originally thought was such a simple task. But I really hope to learn from this.
Many thanks in advance for your advice and tips.
Your thinking along the right lines. Recursion is definetly the way to go. I have implemented a suggested solution below, which should give you the desired output.
var values = ['h1,h2', 'span,style'];
function merge(input, index) {
var nextIndex = index + 1;
var outputArray = [];
var currentArray = [];
if(index < input.length) {
currentArray = input[index].split(',');
}
for(var i = 0, end = currentArray.length; i < end; i++) {
if(nextIndex < input.length) {
merge(input, nextIndex).forEach(function(item) {
outputArray.push(currentArray[i] + ' ' + item);
});
}
else {
outputArray.push(currentArray[i]);
}
}
return outputArray;
}
var output = merge(values, 0, '');
console.log(output);

How do I avoid looping through an array to find a partial match?

I am looping through an array of english phrases, and if i find a match, with the current text node, i replace it with it's translation in the non_english array. All of that works 100% for exact matches.
But for partial matches, I need to use the .match command, which allows for partial matches.
My code to search for exact matches is like this:
var found = $.inArray(value,en_lang);
Then if there is a found value, then do replacement of text. This method is fast and I love it.
However to do partial word/phrase matching, I have to use this looping code.
// loop thru language arrays
for (var x = en_count; x > 0; x--) {
// assign current from/to variables for replace
var from = en_lang[x];
var to = other_lang[x];
// if value match do translation
if (value.match(from)) {
content(node, value.replace(from, to));
}
// mark this node as translated
if ($.browser.msie == 'false') {
$(node).data('translated', 'yes');
}
}
This does the job but is pretty slow. After a lot of research, I have found that I can convert the english array to a list-based string via the join command.
But I am unable to come up with a function to search this list for a partial match, and return the position in the list.
I was trying out this old js function created in 2006. But I can't figure out how to get the position back, correctly.
function listfind(list, value, delimiters) {
if (!delimiters) {
var delimiters = ','
}
_TempListSplitArray = list.split(delimiters)
var FoundIdx = 0;
for (i = 0; i < _TempListSplitArray.length; i++) {
if (_TempListSplitArray[i] == value) {
FoundIdx = i + 1;
break
}
if (value.match(_TempListSplitArray[i])) {
FoundIdx = i + 1;
break
}
}
return FoundIdx
}
Thank you for your time.
Javascript has a foreach type of system but its still based on a loop
var array = ['hello', 'world'];
for(var key in array){
alert(array[key]);
}
Thats the best your getting for looping though an array but this way allso works with objects
var obj = {'one':'hello', 'two':'world'];
for(var key in obj){
alert("key: "+key+" value: "+obj[key]);
}
UPDATED for Comments on your question
You can just replace the text you know
var str = "hello World";
str = str.replace("hello", "Bye bye");
alert(str);

Categories

Resources