ReplaceAll not replacing all in recursive function - javascript

I'm using a recursive function that takes a template string like this:
<div class='${classes}'>
and replaces the template literal placeholder ${classes} with valued from an object that looks like this:
{ classes: 'bg-${color} text-${color}', color: 'blue' }
Something to note here is that within the object, there is more template string nesting, hence the reason for the recursive function.
The thing I cannot work out is why the replace all function is only replacing the 1st instance of ${color}, and leaving the second undefined.
Working snippet below:
function buildTemplate(string) {
var matched = false;
string = string.replaceAll(/\$\{(.*?)\}/g, function (match, key) {
if (match) {
matched = true;
}
const selection = selectors[key];
delete selectors[key];
return selection;
});
if (matched) {
string = buildTemplate(string);
}
console.log(string);
}
let templateString = "<div class='${classes}'>";
const selectors = { classes: 'bg-${color} text-${color}', color: 'blue' }
buildTemplate(templateString);

You are deleting selectors, when you need it twice.
Remove line;
// delete selectors[key]
or selection get assigned nothing
No need to delete anything when it's an object that is accessed through a key.

Related

How to replace text with substitute that contains JSX code?

interface ICard {
content: string,
blanks: Array<{word: string, hidden: boolean}>
}
function processCards():Array<any>{
if (cards !==null ){
const text = cards.map((card,cardIndex)=>{
var content = card.content
card.blanks.map((blank,blankIndex)=>{
// replace content
const visibility = (blank.hidden)?'hidden':'visible'
const click_blank = <span className={visibility} onClick={()=>toggleBlank(cardIndex,blankIndex)}>{blank.word}</span>
content = content.replace(blank.word,click_blank)
})
return content
})
return text
} else {
return []
}
}
I have an array of objects of type ICard.
Whenever card.blanks.word appears in card.content, I want to wrap that word in tags that contain a className style AND an onClick parameter.
It seems like I can't just replace the string using content.replace like I've tried, as replace() does not like the fact I have JSX in the code.
Is there another way to approach this problem?
You need to construct a new ReactElement from the parts of string preceding and following each blank.word, with the new span stuck in the middle. You can do this by iteratively building an array and then returning it wrapped in <> (<React.Fragment>). Here's a (javascript) example:
export default function App() {
const toggleBlankPlaceholder = (cardIndex, blankIndex) => {};
const cardIndexPlaceholder = 0;
const blanks = [
{ word: "foo", hidden: true },
{ word: "bar", hidden: false },
];
const content = "hello foo from bar!";
const res = [content];
for (const [blankIndex, { word, hidden }] of blanks.entries()) {
const re = new RegExp(`(.*?)${word}(.*)`);
const match = res[res.length - 1].match(re);
if (match) {
const [, prefix, suffix] = match;
res[res.length - 1] = prefix;
const visibility = hidden ? "hidden" : "visible";
res.push(
<span
className={visibility}
onClick={() =>
toggleBlankPlaceholder(cardIndexPlaceholder, blankIndex)
}
>
{word}
</span>
);
res.push(suffix);
}
}
return <>{res}</>;
}
The returned value will be hello <span class="hidden">foo</span> from <span class="visible">bar</span>!
A couple of things:
In your example, you used map over card.blanks without consuming the value. Please don't do that! If you don't intend to use the new array that map creates, use forEach instead.
In my example, I assumed for simplicity that each entry in blanks occurs 0 or 1 times in order in content. Your usage of replace in your example code would only have replaced the first occurrence of blank.word (see the docs), though I'm not sure that's what you intended. Your code did not make an ordering assumption, so you'll need to rework my example code a little depending on the desired behavior.

How to filter the parent and its associated children from mat-tree angular material?

I have created mat tree inside the drop down and reated input field to do the filter. The filter works, if i type the full text of parent/child name from the json. But, I want to do the filter while typing the character itself. can find the sample code in stackblitz on below:
https://stackblitz.com/edit/angular-material-tree-dropdown?file=src%2Fapp%2Fapp.component.ts
Anyone help will be Appreciated!!!
You can use basic regexp for this case. Just change your filter function into like this:
public filter(filterText: string) {
let filteredTreeData;
if (filterText) {
// Filter the tree
function filter(array, text) {
const getChildren = (result, object) => {
var re = new RegExp(text, 'gi');
if (object.item.match(re)) {
result.push(object);
return result;
}
if (Array.isArray(object.children)) {
const children = object.children.reduce(getChildren, []);
if (children.length) result.push({ ...object, children });
}
return result;
};
return array.reduce(getChildren, []);
}
filteredTreeData = filter(this.treeData, filterText);
} else {
// Return the initial tree
filteredTreeData = this.treeData;
}
// Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
// file node as children.
const data = filteredTreeData;
// Notify the change.
this.dataChange.next(data);
}
Basically I've change the main filter conditional from this:
if (object.item .toLowerCase() === text.toLowerCase() ) {
result.push(object);
return result;
}
to this:
var re = new RegExp(text, 'gi');
if (object.item.match(re)) {
result.push(object);
return result;
}
What it does is that instead of requiring to match the full text, using simple regex we can output elements that are partially matched.
You can see a working StackBlitz here.

ReactJS: Join map output with concatenating value

In my ReactJS application I am getting the mobile numbers as a string which I need to break and generate a link for them to be clickable on the mobile devices. But, instead I am getting [object Object], [object Object] as an output, whereas it should be xxxxx, xxxxx, ....
Also, I need to move this mobileNumbers function to a separate location where it can be accessed via multiple components.
For example: Currently this code is located in the Footer component and this code is also need on the Contact Us component.
...
function isEmpty(value) {
return ((value === undefined) || (value === null))
? ''
: value;
};
function mobileNumbers(value) {
const returning = [];
if(isEmpty(value))
{
var data = value.split(',');
data.map((number, index) => {
var trimed = number.trim();
returning.push(<NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>);
});
return returning.join(', ');
}
return '';
};
...
What am I doing wrong here?
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
First question:
What am I doing wrong here?
The issue what you have is happening because of Array.prototype.join(). If creates a string at the end of the day. From the documentation:
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
Think about the following:
const navLinks = [{link:'randomlink'}, {link:'randomlink2'}];
console.log(navLinks.join(','))
If you would like to use concatenate with , then you can do similarly like this:
function mobileNumbers(value) {
if(isEmpty(value)) {
const data = value.split(',');
return data.map((number, index) => {
const trimed = number.trim();
return <NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>;
}).reduce((prev, curr) => [prev, ', ', curr]);
}
return [];
};
Then you need to use map() in JSX to make it work.
Second question:
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
Usually what I do for constants is that I create in the src folder a file called Consts.js and put there as the following:
export default {
AppLogo: 'assets/logo_large.jpg',
AppTitle: 'Some app name',
RunFunction: function() { console.log(`I'm running`) }
}
Then simply import in a component when something is needed like:
import Consts from './Consts';
And using in render for example:
return <>
<h1>{Consts.AppTitle}</h1>
</>
Similarly you can call functions as well.
+1 suggestion:
Array.prototype.map() returns an array so you don't need to create one as you did earlier. From the documentation:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I hope this helps!

JavaScript: Finding successive matches with .exec()

I have an object with a myriad of properties, such as color and brand, that describes a product. I'm looking for a way to dynamically generate product descriptions in paragraph form (because API doesn't provide one), and I came up with a way to do so by writing "templates" that have "props" surrounded in brackets {{}}. I wrote a function to "parse" the template by injecting the object properties in the string by replacing the "props" with the value of the key.
For example:
Object: {color: 'white'}
Template: "The bowl is {{color}}."
Result: "The bowl is white."
For some reason, my parse function isn't working. {{general_description}} isn't parsed.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
I followed the example provided in the MDN docs under Examples > Finding successive matches. It says that I need to first store the regular expression in a variable (e.g., templatePropRe), for the expression cannot be in the while loop condition or it will loop indefinitely. However, if I do that, my problem is resolved. See here...nothing broke.
I rewrote the function using String.prototype.match, and it works as expected, but I don't have access to the capture so I need to first strip off the brackets using stripBrackets. See the working example using match here.
What I want to know is why doesn't my parse() function that utilizes RegExp.prototype.exec work properly?
Remove the /g flag from your regex. According to the documentation, when this flag is present, it updates the regex object's lastIndex property which indicates from where next call to exec() will start to search a match.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
This happened because you modify and check the same string in your code.
Whereas regExp saves index of matched substring after each execution you change length of the string and regEx with next execution starts from other point than you expect.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
var resultStr = template;
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
resultStr = resultStr.replace(match, obj[key]);
}
return resultStr;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
Instead of performing the 2-step replacement (finding a match and then replacing the first occurrence with the required value) (that is prone with issues like the one you encountered when a new string is passed to the same RegExp with old, already invalid, index), you may use a callback method as a replacement argument inside a String#replace method. That way, the resulting string will be constructed on the fly upon each match making the code execute faster.
See an example fix below:
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const parse = (template) => {
return template.replace(/{{(\w*)}}/g, ($0, $1) => obj[$1] ? obj[$1] : $0 );
// ES5 way:
// return template.replace(/{{(\w*)}}/g, function($0, $1) {
// return obj[$1] ? obj[$1] : $0;
// });
}
console.log(parse('{{keep}} This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
Note that here, after finding a match, the ($0, $1) => obj[$1] ? obj[$1] : $0 code does the following: the whole match is assigned to $0 variable and the Group 1 value is assigned to $1; then, if there is a key with the name $1 in obj, the value will be put instead of the match into the right place in the resulting string. Else, the whole match is put back (replace with '' if you want to remove a {{...}} with a non-existent key name).

How do I get a constructor value to other methods to be modified?

The goal of this project is to refactor a previous solution to work with actual objects. Currently when I run the Jasmine tests I get these two errors:
TypeError: Cannot read property 'split' of undefined
TypeError: Cannot set property 'title' of undefined
Why is the class not recognizing the title value when I attempt to pass it into other methods? Prior to me trying to send the value into other methods it seemed to work but now that I am trying to send the string value to the titleCreator method it keeps on returning undefined.
class bookTitle {
constructor(title) {
this.title = this.titleCreator(title); // this sets a title value to the bookTitle object/class
}
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var modifiedString = this.string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
// This function just capitalizes the word given to it
}
}
module.exports = {
bookTitle
}
Edit: Here are my Jasmine test cases for context. Idea of the program is just to pass these cases
var bookTitles = require ('./bookTitles.js');
describe('bookTitle', function() {
var book; // this is the object that will be passed into the test cases, returns undefined here without beforeEach
beforeEach(function() {
book = new bookTitles.bookTitle(); // creates a new book instance before each test is run
});
describe('title', function() {
it('should capitalize the first letter', function() {
book.title = 'inferno';
expect(book.title).toEqual('Inferno'); // works without capitalizing
});
it('should capitalize every word', function() {
book.title = 'stuart little';
expect(book.title).toEqual('Stuart Little');
});
describe('should capitalize every word except...', function() {
describe('articles', function() {
it('does not capitalize "the"', function() {
book.title = 'alexander the great';
expect(book.title).toEqual('Alexander the Great');
});
it('does not capitalize "a"', function() {
book.title = 'to kill a mockingbird';
expect(book.title).toEqual('To Kill a Mockingbird');
});
it('does not capitalize "an"', function() {
book.title = 'to eat an apple a day';
expect(book.title).toEqual('To Eat an Apple a Day');
});
});
it('conjunctions', function() {
book.title = 'war and peace';
expect(book.title).toEqual('War and Peace');
});
it('prepositions', function() {
book.title = 'love in the time of cholera';
expect(book.title).toEqual('Love in the Time of Cholera');
});
});
describe('should always capitalize...', function() {
it('I', function() {
book.title = 'what i wish i knew when i was 20';
expect(book.title).toEqual('What I Wish I Knew When I Was 20');
});
it('the first word', function() {
book.title = 'the man in the iron mask';
expect(book.title).toEqual('The Man in the Iron Mask');
});
});
});
});
You are trying to access this.string in this line of code:
var modifiedString = this.string
before you have set this.string to have any value. Perhaps you meant to just use string, the argument passed to titleCreator. this.string is not the same as string. Since this.string has never been assigned, it is undefined and therefore any attempt to access a method on it will fail.
It's a little hard to know exactly what your intent was, but perhaps you meant to use use string instead of this.string:
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var modifiedString = string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
From your error description, it also sounds like you may have an issue with how you call bookTitle (it should be called as a constructor as in
let bk = new (yourModule.bookTitle)("some string")
If you want help with that, please show the calling code that calls that constructor so we can advise on that too.
Here's a working piece of code that I had to fix several other things in:
class bookTitle {
constructor(title) {
this.title = this.titleCreator(title); // this sets a title value to the bookTitle object/class
}
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var self = this;
var modifiedString = string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return self.capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return self.capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
// This function just capitalizes the word given to it
}
}
let bookTitles = {
bookTitle: bookTitle
};
let book = new bookTitles.bookTitle("some title of the book");
console.log(book)
Things I had to fix:
Change this.string.split(...) to string.split(...).
Define self to be this.
Use self.capitalize() instead of capitalize() to call method properly (in two places)
Pass in a string when calling the constructor (your code was calling the constructor with no parameters which makes an error in your constructor). Your code requires a string be passed to the constructor.
In addition, it appears that your code thinks that just assigning to the .title property will somehow run the titleCreator() method and make the proper capitalization. It will not. Assigning to the .title property just sets that property. It doesn't run any of your methods. You could define a setter method to make it run code when you assign to the property, but it would probably make more sense to just create a setTitle() method that does what you want (calls .titleCreator() and assigns the result to .title).

Categories