I am trying to build an autocomplete component where the relevant characters are highlighted in the search suggestions as the user types.
The way I have come up to highlight the characters is this JSX
{suggestion.substring(0, ind) +
<mark>{suggestion.substring(ind, val.length)}</mark> +
suggestion.substring(val.length, suggestion.length)}
but it renders an `[Object Object] instead of the string.
How can I correct this?
Here's my whole code
searchSuggestions
.filter((suggestion) => {
const lowerSuggestion = suggestion.toLowerCase();
const val = eventValue.toLowerCase();
console.log("lowersuggestion is", lowerSuggestion, val);
return lowerSuggestion.indexOf(val) > -1;
})
.map((suggestion) => {
const lowerSuggestion = suggestion.toLowerCase();
const val = eventValue.toLowerCase();
const ind = lowerSuggestion.indexOf(val);
console.log("ind is", ind, typeof suggestion);
return (
<div>
{suggestion.substring(0, ind) +
<mark>{suggestion.substring(ind, val.length)}</mark> +
suggestion.substring(val.length, suggestion.length)}
</div>
);
})
Assuming searchSuggestions is an array of string. How should I go about it?
<mark is a JSX element - which is an object. When you interpolate it (or most objects) into a string, you'll get [object Object].
Use an array instead, so that it gets interpolated into the JSX, rather than interpolated into a string.
<div>
{[
suggestion.substring(0, ind),
<mark>{suggestion.substring(ind, val.length)}</mark>,
suggestion.substring(val.length, suggestion.length)
]}
</div>
There was a problem with the JSX itself, I solved it by changing it to
{suggestion.substring(0, ind)}
<mark style={{ backgroundColor: "#f7e7d0" }}>
{suggestion.substring(ind, ind + val.length)}
</mark>
{suggestion.substring(ind + val.length, suggestion.length)}
Related
I am wanting to take a string and get the JSX to replace all the words in between brackets to be bold. I got it to work with this, but just wondering if there is a better way of going about this?
const jsxArray = [];
let unformattedString = "[name] Hi there, my name is [name], I am [age] years old, and I work in the field of [profession]";
const boldStrings = unformattedString.match(/(?<=\[).+?(?=\])/g);
const notBoldStrings = unformattedString.split(/\[.+?\]/g);
let j = 0;
if (boldStrings !== null) {
notBoldStrings.forEach((notBoldString, index) => {
if (index === 0 && notBoldStrings === "") {
// the first word should be bolded
jsxArray.push(<b>{boldStrings[j]}</b>);
} else {
jsxArray.push(notBoldString);
jsxArray.push(<b>{boldStrings[j]}</b>);
}
j++;
});
} else {
jsxArray.push(notBoldStrings[0]);
}
The expected output would be:
name Hi there, my name is name, I am age years old, and I work in the field of profession
You can use this code:
export default function App() {
let unformattedString =
"[name] Hi there, my name is [name], I am [age] years old, and I work in the field of [profession]";
const boldString = (str, substr) =>{
str.replaceAll(substr, `<b>${substr}</b>`);
}
const strArr = unformattedString.split(" ");
const formattedString = strArr
.map((item) => {
let strBold = item.match(/(?<=\[).+?(?=\])/g);
if (strBold) {
return boldString(item, strBold);
} else {
return item;
}
})
.join(" ");
return (
<div className="App">
<div dangerouslySetInnerHTML={{ __html: formattedString }} />
</div>
);
}
To see its result: codesandbox.io
But about dangerouslySetInnerHTML, Setting HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack. And recommended using the html-react-parser package in your React project.
npm install html-react-parser
To use:
import parse from 'html-react-parser'
const yourHtmlString = '<h1>Hello</h1>'
and in return part:
<div>
{parse(yourHtmlString)}
</div>
I'm doing some string manipulation with API returned fields, and they have some unique characteristics, hence why I'm looking to try to adjust them.
For example:
one field comes from the JSON as: "contract_time": "full_time"
Provided this, I would like to try and manipulate it so that I get the output "Full Time".
I am calling it directly as the below:
<BadgeComponentFirst>
<Typography color="red" fontSize="0.6em">
{job.contract_time}
</Typography>
How would I pass such string manipulation to an object to first, remove the '_' and capitalise and the first of each word?
Thanks
The approach you need should differ if the API values for that field are a known in advance or not.
If the values are known in advance, use an object to map the known values to their user-facing equivalent:
const CONTRACT_TIMES = {
full_time: "Full Time",
part_time: "Part Time",
};
<Typography color="red" fontSize="0.6em">
{CONTRACT_TIMES[job.contract_time] || "Unknown"}
</Typography>
If the API can return any value and you just want to display a cleaned up version, then write a function that does the manipulation you need:
function getFriendly(str) {
return str.split("_").map(getFriendlyWord).join(" ");
}
function getFriendlyWord(word) {
return word.slice(0, 1).toUpperCase() + word.slice(1);
}
<Typography color="red" fontSize="0.6em">
{getFriendly(job.contract_time)}
</Typography>
You want to split each word on '_' into an array, you can then map over the array and capitalize the first letter of each word. Then you want to join the words back together, separated by spaces. You can do:
let job = {
contract_time: "full_time"
}
console.log(job.contract_time.split("_").map(word => word[0].toUpperCase() + word.substr(1)).join(' '));
console: Full Time
define a method transforms your data:
const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
const verbose = (snake_cased) => {
snake_cased.split('_').map(capitalize).join(' ');
};
And then you are free to use this transformer anywhere inside of JSX code without any limits. The only thing should keep in mind - this function will be fired every render. If this calculation is complicated - you may need some optimisation techniques.
const Component = () => (
<BadgeComponentFirst>
<Typography color="red" fontSize="0.6em">
{verbose(job.contract_time)}
</Typography>
</BadgeComponentFirst>
);
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.
I need to search a string, and if it has any values that match my array, I need to add <span></span> tags to them to add custom CSS. I am using reactJS.
How do I search the string for objects from my array?
Example:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'},...]
findAllOccurrances = () => {???}
Then systematically replace them '{{inputX}}' with <span className='bizarre-highlight'>{{inputX}}</span>
My intent is to add custom CSS to any text in the div which matches my array, so if you got any ideas please shoot! Again, using reactJS if that helps.
I created a component that will replace the elements that need to be highlighted with a span you can test it here
The component is:
import React from 'react';
export default ({ terms, children }) => {
const result = []
const regex = terms.map(escapeRegExp).join('|');
const re = new RegExp(regex);
let text = (' ' + children).slice(1); // copy
let match = re.exec(text);
while (match != null) {
const str = match.toString();
result.push(text.slice(0, match.index));
result.push(<span className="highlighted">{str}</span>);
text = text.slice(match.index + str.length);
match = re.exec(text);
}
result.push(text);
return result;
}
function escapeRegExp (str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
And you should use it like this:
import React from 'react';
import Highlighter from './Highlighter';
const terms = [ '{{input1}}', '{{input2}}' ]
const App = () => (
<div>
<Highlighter terms={terms}>
{'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'}
</Highlighter>
</div>
);
Use String#replace with a RegExp to find all instances of '{{inputX}}', and wrap the matches with the span:
const string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input3}}'
const array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}]
const pattern = new RegExp(array.map(({ parameter }) => parameter).join('|'), 'g');
const result = string.replace(pattern, (match) =>
`<span className='bizarre-highlight'>${match}</span>`
)
console.log(result)
use Array#map to extract values for wrapping in <span> and then cycle on them for replacement:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}';
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}];
array.map(el => { return el.parameter }).forEach(str => {
string = string.split(str).join("<span className=\'bizarre-highlight\'>" + str + "</span>");
});
console.log(string);
What is the proper way to replace urls in a string and render them as links with React?
Say I have a string: 'hello http://google.com world', and I want it to render like: hello http://google.com world
Ok, so this is how I done it.
class A extends React.Component {
renderText() {
let parts = this.props.text.split(re) // re is a matching regular expression
for (let i = 1; i < parts.length; i += 2) {
parts[i] = <a key={'link' + i} href={parts[i]}>{parts[i]}</a>
}
return parts
}
render() {
let text = this.renderText()
return (
<div className="some_text_class">{text}</div>
)
}
}
I ran into issues with every answer here, so I had to write my own:
// use whatever you want here
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9#:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()#:%_\+.~#?&//=]*)/;
const renderText = txt =>
txt
.split(" ")
.map(part =>
URL_REGEX.test(part) ? <a href={part}>{part} </a> : part + " "
);
There are NPM modules to handle this. Both of these depend on linkify-it (repo)
react-linkify (repo)
<Linkify>
<div>react-linkify <span>(tasti.github.io/react-linkify/)</span></div>
<div>React component to parse links (urls, emails, etc.) in text into clickable links</div>
See examples at tasti.github.io/react-linkify/.
<footer>Contact: tasti#zakarie.com</footer>
</Linkify>
At time of writing, the current version is 1.0.0-alpha. It requires React 16. The repo has 14 open tickets and 17 open PRs. So that's not fantastic.
Version 0.2.2 allows much earlier versions but doesn't have link text decoration, etc.
react-native-hyperlink ( repo )
If you are using native (ie a phone app), it looks like the better of the two options. Code samples:
<Hyperlink linkDefault={ true }>
<Text style={ { fontSize: 15 } }>
This text will be parsed to check for clickable strings like https://github.com/obipawan/hyperlink and made clickable.
</Text>
</Hyperlink>
<Hyperlink onLongPress={ (url, text) => alert(url + ", " + text) }>
<Text style={ { fontSize: 15 } }>
This text will be parsed to check for clickable strings like https://github.com/obipawan/hyperlink and made clickable for long click.
</Text>
</Hyperlink>
<Hyperlink
linkDefault
injectViewProps={ url => ({
testID: url === 'http://link.com' ? 'id1' : 'id2' ,
style: url === 'https://link.com' ? { color: 'red' } : { color: 'blue' },
//any other props you wish to pass to the component
}) }
>
<Text>You can pass props to clickable components matched by url.
<Text>This url looks red https://link.com
</Text> and this url looks blue https://link2.com </Text>
</Hyperlink>
References
https://github.com/facebook/react-native/issues/3148
REACT - How to replace URL strings to <a> elements and rendering it properly
Try this library, it does exactly you need:
https://www.npmjs.com/package/react-process-string
An example from there:
const processString = require('react-process-string');
let config = [{
regex: /(http|https):\/\/(\S+)\.([a-z]{2,}?)(.*?)( |\,|$|\.)/gim,
fn: (key, result) => <span key={key}>
<a target="_blank" href={`${result[1]}://${result[2]}.${result[3]}${result[4]}`}>{result[2]}.{result[3]}{result[4]}</a>{result[5]}
</span>
}, {
regex: /(\S+)\.([a-z]{2,}?)(.*?)( |\,|$|\.)/gim,
fn: (key, result) => <span key={key}>
<a target="_blank" href={`http://${result[1]}.${result[2]}${result[3]}`}>{result[1]}.{result[2]}{result[3]}</a>{result[4]}
</span>
}];
let stringWithLinks = "Watch this on youtube.com";
let processed = processString(config)(stringWithLinks);
return (
<div>Hello world! {processed}</div>
);
That will replace all links with or without "http://" protocol. If you want to replace only links with protocol, remove the second object from config array.
First add <a> tag to string:
function httpHtml(content) {
const reg = /(http:\/\/|https:\/\/)((\w|=|\?|\.|\/|&|-)+)/g;
return content.replace(reg, "<a href='$1$2'>$1$2</a>");
}
console.log(httpHtml('hello http://google.com world'))
// => hello http://google.com world
Then render string as html in react:
function MyComponent() {
return <div dangerouslySetInnerHTML={{
__html: httpHtml('hello http://google.com world')
}} />;
}
I wrote a short function to do it:
const RE_URL = /\w+:\/\/\S+/g;
function linkify(str) {
let match;
const results = [];
let lastIndex = 0;
while (match = RE_URL.exec(str)) {
const link = match[0];
if (lastIndex !== match.index) {
const text = str.substring(lastIndex, match.index);
results.push(
<span key={results.length}>{text}</span>,
);
}
results.push(
<a key={results.length} href={link} target="_blank">{link}</a>
);
lastIndex = match.index + link.length;
}
if (results.length === 0) {
return str;
}
if (lastIndex !== str.length) {
results.push(
<span key={results.length}>{str.substring(lastIndex)}</span>,
);
}
return results;
}
late to the party but here's a slightly modified version :
export const linkRenderer = (string: string):ReactNode => {
const linkExp = /^https?:\/\/[a-z0-9_./-]*$/i
return <>{
string.split(/(https?:\/\/[a-z0-9_./-]*)/gi).map((part, k) => <React.Fragment key={k}>
{part.match(linkExp) ? <a
href={part}
onFocus={(e) => { e.stopPropagation() }}
target="_blank"
rel="noreferrer"
>{part}</a>
: part}
</React.Fragment>)
}</>
}
Interesting things to note there:
it doesn't split on space or blank space so preserving existing spaces
It create less chunks by only splitting parts where link are not every word or so
the regexp passed to split must have capturing parenthesis if you want to have your links as part of the resulting array.
the noreferrer attribute is required with target blank for security reason on older browsers
Hope this help.
Based on the OP's own answer I came up with the one-liner:
{text
.split(/[-a-zA-Z0-9#:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9#:%_\+.~#?&//=]*)?/gi)
.map((part, index) => index % 2 === 0 ? part : {part}
}
for me, i managed to solve it this way
const ActiveProblem = () => {
const { activeProblem } = useProblems();
const id = new Date().toString();
const match = activeProblem.replace(
urlPattern,
(matched) => id + matched + id
);
return (
<Typography align="center" variant="body1" color="white">
{match.split(id).map((str, idx) => {
if (str.match(urlPattern))
return (
<Box component="a" href={str} key={id + idx}>
{str}
</Box>
);
return str;
})}
</Typography>
);
};
I'm using Material UI with React