I am returning a set of information from Spotify in a React Component and want to interrogate the JSON that is returned and highlight the original search term within the artist name. so for example, if you search 'bus' and one of the artists returned is Kate Bush, then this would be highlighted green in 'Kate BUSh'. At the moment I am calling a function from within render(). However, what I get rendered is:
Kate <span style="color:green">Bus</span>h
How do I get render() to read the HTML as HTML (so that Bus would just be green) rather than rendering as text? Relevant code from the React Component below:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
if (displayString.indexOf(searchTerm) !== -1) {
displayString = displayString.replace(searchTerm, '<span style="color:green">'+searchTerm+'</span>');
}
return displayString;
}
render() {
return (
<div className="Track" id="Track">
<div className="Track-information">
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
</div>
);
}
Your underlineSearch function needs to return React Elements, but right now it is returning a string. You could use a Fragment to make it work:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
const searchTerm = this.props.searchTerm;
const indexOfSearchTerm = displayString.indexOf(searchTerm);
let node;
if (indexOfSearchTerm === -1) {
node = displayString;
} else {
node = (
<React.Fragment>
{displayString.substr(0, indexOfSearchTerm)}
<span style={{color: 'green'}}>
{displayString.substr(indexOfSearchTerm, searchTerm.length)}
</span>
{displayString.substr(indexOfSearchTerm + searchTerm.length)}
</React.Fragment>
);
}
return node;
}
To make your solution even more reusable you can make underlineSearch and wrapper with your styles for highlighting into 2 separate components. Even more, you can search for multiple occurrences of your searchTerm with regex. Found a similar SO question here. I slightly adapted one of the answers there according to your needs, but all credit goes to this amazing and neat solution for highlighting matches of a string in longer texts. Here is the code:
const Match = ({ children }) => (
<span style={{'color':'green'}}>{children}</span>
);
const HighlightMatches = ({ text, searchTerm }) => {
let keyCount = 0;
let splits = text.split(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let matches = text.match(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let result = [];
for (let i = 0; i < splits.length; ++i) {
result.push(splits[i]);
if (i < splits.length - 1) {
result.push(<Match key={++keyCount}>{matches[i]}</Match>);
}
}
return (
<p>{result}</p>
);
};
Then in your main component where you render everything you can do this:
render() {
<div className="Track" id="Track">
<div className="Track-information">
<h3>
<HighlightMatches text={this.props.trackName} searchTerm={this.props.searchTerm}/>
</h3>
<p>
<HighlightMatches text={this.props.artistName} searchTerm={this.props.searchTerm} /> |
<HighlightMatches text={this.props.albumName} searchTerm={this.props.searchTerm} />
</div>
</div>
}
To me this seems like the most react-like approach to solve the problem :)
While you can use dangerouslySetInnerHTML (), as the name suggests it is extremely dangerous, since it is prone to XSS attacks, for example:
{artist: "Kate Bush<script> giveMeAllYourCookies()</script>"}
You can split the displayString into an array and render it.
Please note that that my implementation of underlineSearch is buggy, and will not work if there are more than one match.
class Main extends React.Component {
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
var index = 0;
var results = [];
var offset = 0;
while(true) {
const index = displayString.indexOf(searchTerm, offset);
if(index < 0) {
results.push(<span>{displayString.substr(offset)}</span>);
break;
}
results.push(<span> {displayString.substr(offset, index)}</span>);
results.push(<strong style={{color: 'green'}}> {displayString.substr(index, searchTerm.length)}</strong>);
offset = index + searchTerm.length;
}
return results;
}
render() {
return <div>
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
}
}
ReactDOM.render(<Main
trackName="Magic Buses"
artistName="Kate Bush"
albumName="Kate Bush Alubm"
searchTerm="Bus"
/>, document.getElementById('main'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='main'></div>
Related
Hi I'm fairly new to React and currently trying to write a word counter. The idea is that once you type in the text box it will then display a list of all the words and the frequency of how often they're used (This would be displayed in the span tag where it says wordCounts). The issue I'm currently having is it only displays one word with the frequency when I want a list. Moreover I honestly feel like could be achieved in a completely different way but again I'm fairly new to React and learning as I go.
If I need to share any more info or more code, please let me know.
React Code
import { Component } from "react";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
firstValue: "",
numberOfCharacters: "",
withoutWhiteSpace: "",
numberOfWords: "",
linesCount: "",
wordSelectionCount: "",
};
}
firstHandle = (event) => {
var input = event.target.value;
const text = document.getElementById("textarea").value;
const linesCount = text.split("/\r|\r\n|\n/").length;
const numberOfCharacters = input === "" ? 0 : input.split("").length;
const withoutWhiteSpace =
input === "" ? 0 : input.split("").filter((char) => char !== " ").length;
const words =
input === "" ? 0 : input.split(" ").filter((word) => word.trim()).length;
const lines = input === "" ? 1 : input.split(/\n/g).length;
this.setState({
firstValue: input,
numberOfCharacters: numberOfCharacters,
withoutWhiteSpace: withoutWhiteSpace,
numberOfWords: words,
linesCount: lines,
wordSelectionCount: "",
});
};
// This function is responsible for the word counting
wordCounter = (e) => {
e.preventDefault();
var keys = [];
var counts = {};
const input = this.state.firstValue
.replace(/\W/g, " ")
.replace(/[0-9]/g, " ")
.split(" ")
.filter((word) => word.trim());
for (let i = 0; i < input.length; i++) {
var word = input[i];
if (counts[word] === undefined) {
counts[word] = 1;
keys.push(word);
} else {
counts[word] += 1;
keys.push(word);
// console.log(keys);
}
keys.sort();
for (let i = 0; i < keys.length; i++) {
var key = keys[i];
var result = key + " - " + counts[key];
console.log(result);
}
this.setState({
wordSelectionCount: result,
});
}
};
render() {
var numberOfCharacters = this.state.numberOfCharacters;
var withoutWhiteSpace = this.state.withoutWhiteSpace;
var words = this.state.numberOfWords;
var lines = this.state.linesCount;
var wordCounts = this.state.wordSelectionCount;
console.log(wordCounts);
return (
<div className="App">
<header className="App-header">
<form>
<h1>Character Counter</h1>
<p>
Characters <span>{numberOfCharacters}</span> Without White Space{" "}
<span>{withoutWhiteSpace}</span> Words <span>{words}</span> Lines{" "}
<span>{lines}</span>
</p>
<textarea
id="textarea"
type="text"
placeholder="Please type some text..."
value={this.firstValue}
onChange={this.firstHandle}
/>
<h1>Word Counting</h1>
{/* This button calls the wordCounter Method which should display all the Word listings */}
<button className="btn" onClick={this.wordCounter}>
Words Count
</button>
<p>
<span>{wordCounts}</span>
</p>
</form>
</header>
</div>
);
}
}
export default App;
Issue
The issue is you are not iterating over wordSelectionCount to render your data, therefore your latest value will be displayed.
You can iterate over wordSelectionCount to render all of the data. But, I have a suggestion for you
Suggestion
First suggestion will be, use functional components and react hooks.
Second, use the power of the object's [key-value] pair to store the word counts.
I have created a codesandbox example if you want to have a look. You can start adding words to see the word counter.
Solution for the existing code
instead of rendering <span>{wordCounts}</span> directly, you should iterate over it. Such as:
this.state.wordSelectionCount && Object.keys(this.state.wordSelectionCount).map(word => (
<span>{word} - {this.state.wordSelectionCount[word]}</span>
)}
I'm VERY new to React and not sure how to render this nested array from an external JSON file. remittance is the only array here one that has nested values that I need to access and render. It logs to the console fine, but won't separate on setState. I apologize if this looks terrible.
class App extends Component {
constructor(props){
super(props);
this.state = {
data,
rName: "",
rDescription: "",
}
}
componentDidMount(){
console.log("Mounted!");
this.display();
};
display = () => {
for (var i = 0; i < data.length; i++){
var remittanceList = data[i].Remittance;
// console.log(remittanceList);
for (var x = 0; x < remittanceList.length; x++){
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
this.setState({rName, rDescription});
}
}
}
render() {
var { rName, rDescription } = this.state;
return (
<div className="App">
<Title/>
{this.state.data.map((each, index) => (
<PayInfo key={index}
name={each.Payee.Name}
fax={each.Payee.Fax}
pan={each.Payment.PAN}
cvv={each.Payment.CVV}
exp={each.Payment.Exp}
payorName={rName}
description={rDescription}
/>
))}
</div>
);
}
}
export default App;
And the JSON file is something like this. Because the amount of remittances can very, I can't hardcode in an index to look for every time.
[
{
"Payee": {
"Name": "Bob",
"Fax": "5555555555",
},
"Payment": {
"PAN": 123456,
"CVV": 123,
"Exp": "1/2018"
},
"Remittance": [
{
"PayorName": "Me",
"Description": "Hello World.",
},
{
"PayorName": "You",
"Description": "Hey world.",
},
{
"PayorName": "Snoop",
"Description": "Bye world.",
}
]
},
And this is the PayInfo.js file I should've posted initially! Not sure if this changes any of the answer I got before?
import React from "react";
import "./PayInfo.css";
const PayInfo = props => (
<div>
<div id="payee">
<p>Payee</p>
<p>{props.name}</p>
<p>Fax: {props.fax}</p>
</div>
<hr></hr>
<div id="payment">
<p>Payment</p>
<p>PAN: {props.pan}</p>
<p>CVV: {props.cvv}</p>
<p>Exp: {props.exp}</p>
</div>
<hr></hr>
<div id="remittance">
<p><strong>Remittance(s)</strong></p>
<p>Payor: {props.payorName}</p>
<p>Description: {props.description} </p>
</div>
<hr></hr>
<hr></hr>
</div>
);
export default PayInfo;
In your display method, you are setting the state inside for loop. so only the last value being set to the state.
Please try the below code.
display = () => {
let totalRemittanceArray = []
for (var i = 0; i < data.length; i++) {
var remittanceList = data[i].Remittance;
for (var x = 0; x < remittanceList.length; x++) {
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
totalRemittanceArray.push({ rName, rDescription })
}
}
this.setState({totalRemittanceArray})
}
What you might want to do is something like this:
https://codesandbox.io/s/p36jlrz7jj
As you can see I extracted another component PayerInfo. Also React works perfect with map within the render method. I hope my example helps!
Currently you call setState in the for loop and thus overwrite the state every time, but I guess you figured that one out already. I use Array.map to loop over elements within JSX a lot. Dunno if one could do this more nicely, but there is always a possibility to improve ^^
A small improvement here: You could loop in a separate function first and then return the complete Array: https://reactjs.org/blog/2017/09/26/react-v16.0.html
In one of my react apps I have to loop through an array.
function ActionTags({tags}) {
let thisTagsHtml = (tags);
//thisTagsHTML is a simple string, separated by ##
//string1##string2##string3##string4
let tagsArray = thisTagsHtml.split('##');
console.log(tagsArray);
return (
<div>
{tagsArray.map(function(item, i){
<span key = {i}>{item}</span>
})}
</div>
);
}
This looks pretty simple. However, nothing is returned from the function. Any idea where my mistake is? Thank you.
you missed return keyword before the statement <span key = {i}>{item}</span>
Like this :
function ActionTags({tags}) {
let thisTagsHtml = (tags);
//thisTagsHTML is a simple string, separated by ##
//string1##string2##string3##string4
let tagsArray = thisTagsHtml.split('##');
console.log(tagsArray);
return (<div>
{
tagsArray.map(function(item, i) {
return <span key={i}>{item}</span>
});
}
</div>);
}
I want to display 8 sports in a list and display it's status participated when user already participated. Display a button when user not participated.But i didn't succeed it with the inner map function
{this.state.sports.map(function(sport, index){
return(
<div>
<li key={index} style={{ listStyle:'none' }}>
<h4>{ sport.name }</h4>
</li>
{this.state.join_sports.map(function(sport1, index){
return (
<div>
{sport._id === sport1.s_id ?
<span>Participated</span> :
<button className="btn btn-info btn-xs">Participate</button>
}
</div>
)
}.bind(this))}
</div>
);
}.bind(this))}
I'm going to take a stab at this and simplify it with some ES6 syntax and some pseudo code markup
I think your map function looks like this:
mapSportsToUser() {
const sports = ['a', 'b', ... 'z']//pseudo data
const user = ['b', 'c'] //we should only show sport c and c
return (
<container> //not needed in 15.5+ I think
{ sports.map( (sport, sportIndex) => ( //this will generate one title for each sport
<title>{sport.name}</title>
//your second map iterates over the list...
//(probably not the easiest way to do this but it should work)
{ user.map( (sportUser, userIndex) => {
if (sportUser == sport) {
return <participate />
} else {
return null //don't render anything
}
} }
)) }
</container>
)
}
I think you're making this really complex by looping over the everything rather than building the data in a way that makes sense from the start.
You might consider something like this:
structureData(listOfSports, listUserHasParticipatedIn) {
let sportsLookup = {} //create a map
for(let i = 0; i < listOfSports.length; i++) {//or whatever iterator you like
const sport = listOfSports[i]
sportsLookup[sport.id] = {
sport: sport,
participation: false,
}
}
for(let i = 0; i < listUserHasParticipatedIn.length; i++) {
const user_sport = listUserHasParticipatedIn[i]
//you might want to do a null check here
sportsLookup[user_sport.id].participation = true;
}
return sportsLookup
}
Then you have something easier to map which looks like:
[{ sport: 'a', participation: false}, { sport: 'b', participation: true} ...]
....
sportsLookup.map( lookup => { //much simpler
return <container> {lookup.sport.name} {lookup.participation ? 'Yes' : 'No'}</container>
})
I got succeeded in the inner map looping technique. Just take a variable make it false initially and make it true when the condition is true. That's all. Thank you.
{ this.state.sports.map((sport,index)=>{
let is_participate = false;
this.state.join_sports.map((j_spt,index)=>{
if(sport._id === j_spt.s_id[0]){
is_participate = true;
}
});
return(
<li>{is_participate? <span>{sport.name}-- participated</span>:<span>{sport.name}-- participate </span>}</li>
)
})
}
How to transform this:
{dataFormat: 'hello my [friend=https://en.wikipedia.org/wiki/Friendship]'}
to this:
<div>
hello my <a onClick={...} href="https://en.wikipedia.org/wiki/Friendship">friend</a>
</div>
I need to somehow be able to scan a string and create links on the fly. Any idea?
The dataFormat can contain more than one link with unknown order between "regular" text and links.
Ended up using regex which did the job.
JSBin: https://jsbin.com/yogepa/edit?js,output
Code:
renderSpan(content) {
return <span>
{content}
</span>
}
renderLink(content) {
const parts = content
.replace(/\[|\]/g, '')
.split('=');
return <a style={ styles.link } onClick={ alert }>
{parts[0]}
</a>
}
renderFormat() {
let { dataFormat } = this.state;
const regex = /(\[[^\]]+])*([^\[]+)(\[[^\]]+])*(\[[^\]]+])*([^\[]+)(\[[^\]]+])*(\[[^\]]+])*([^\[]+)(\[[^\]]+])*/;
const matches = regex.exec(dataFormat);
return matches.reduce((output, match, index) => {
if (match && index >= 2) {
output.push(match.indexOf('[') >= 0 ?
this.renderLink(match) :
this.renderSpan(match)
);
}
return output;
}, []);
}
I probably can improve the Regex expression though.