This question already has answers here:
How to render react components by using map and join?
(18 answers)
Closed 10 months ago.
I'd like to inject a separator between each element of an array using an array.join-like syntax.
Something like the following:
render() {
let myArray = [1,2,3];
return (<div>
{myArray.map(item => <div>{item}</div>).join(<div>|</div>)}
</div>);
}
I've got it working using a lodash.transform approach, but it feels ugly, I'd like to just do .join(<some-jsx>) and have it insert a separator in between each item.
You can also use reduce to insert the separator between every element of the array:
render() {
let myArray = [1,2,3];
return (
<div>
{
myArray
.map(item => <div>{item}</div>)
.reduce((acc, x) => acc === null ? [x] : [acc, ' | ', x], null)
}
</div>
);
}
or using fragments:
render() {
let myArray = [1,2,3];
return (
<div>
{
myArray
.map(item => <div>{item}</div>)
.reduce((acc, x) => acc === null ? x : <>{acc} | {x}</>, null)
}
</div>
);
}
You can also do it by combining .reduce and React fragments.
function jsxJoin (array, str) {
return array.length > 0
? array.reduce((result, item) => <>{result}{str}{item}</>)
: null;
}
function jsxJoin (array, str) {
return array.length > 0
? array.reduce((result, item) => <React.Fragment>{result}{str}{item}</React.Fragment>)
: null;
}
const element = jsxJoin([
<strong>hello</strong>,
<em>world</em>
], <span> </span>);
ReactDOM.render(element, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Trying to .join with React Elements is probably not going out pan out for you. This would produce the result you describe needing.
render() {
let myArray = [1,2,3];
return (
<div>
{myArray.map((item, i, arr) => {
let divider = i<arr.length-1 && <div>|</div>;
return (
<span key={i}>
<div>{item}</div>
{divider}
</span>
)
})}
</div>
);
}
Using Array.map and React.Fragment you can join each array item with any JSX element you want. In the example below I'm using a <br /> tag but this can be substituted for whatever element you want.
const lines = ['line 1', 'line 2', 'line 3'];
lines.map((l, i) => (
<React.Fragment key={i}>{l}{i < (lines.length - 1) ? <br /> : ''}</React.Fragment>
));
The output would be
line 1 <br />
line 2 <br />
line 3
Btw. you can also supply functions to react. My approach would be .forEach pairs of push(value); push(glue);, and afterwards pop() the trailing glue...
function() {
joinLike = [];
originData.forEach(function(v,i) {
joinLike.push(v);
joinLike.push(<br>);
})
joinLike.pop();
return joinLike;
}
To produce the requested solution, in React join doesn't seem to work so with map and reduce you can do it like this:
render() {
let myArray = [1, 2, 3];
return (
<div>
{myArray
.map(item => <div>{item}</div>)
.reduce((result, item) => [result, <div>|</div>, item])}
</div>
);
}
You could also try flatMap(). Browser support is coming, but until then you could use a polyfill. Only downside is you'd have to lose the last element.
Eg.
{myArray.flatMap(item => [<div>{item}</div>, <div>|</div>]).slice(0, -1)}
Or
{myArray.flatMap((item, i) => [
<div>{item}</div>,
i < myArray.length - 1 ? <div>|</div> : null
])
You can use a function like this one
function componentsJoin(components, separator) {
return components.reduce(
(acc, curr) => (acc.length ? [...acc, separator, curr] : [curr]),
[]
);
}
Or can use a package, found this one
https://www.npmjs.com/package/react-join
If you are using TypeScript, you can simply copy this file and use jsxJoin:
import { Fragment } from "react";
/**
* Join together a set of JSX elements with a separator.
*
* #see https://stackoverflow.com/q/33577448/5506547
*/
function jsxJoin(components: JSX.Element[], separator: any) {
// Just to be sure, remove falsy values so we can add conditionals to the components array
const filtered = components.filter(Boolean);
return filtered.length === 0
? null
: (filtered.reduce(
(acc, curr, i) =>
acc.length
? [
...acc,
// Wrap the separator in a fragment to avoid `missing key` issues
<Fragment key={i}>{separator}</Fragment>,
curr
]
: [curr],
[]
) as JSX.Element[]);
}
export { jsxJoin };
// typescript
export function joinArray<T, S>(array: Array<T>, separator: S): Array<T | S> {
return array.reduce<(T | S)[]>((p, c, idx) => {
if (idx === 0) return [c];
else return [...p, separator, c];
}, []);
}
// javascript
export function joinArray(array, separator) {
return array.reduce((p, c, idx) => {
if (idx === 0)
return [c];
else
return [...p, separator, c];
}, []);
}
// example
console.log(joinArray(["1", "2", "3"], 2));
// -> ["1", 2, "2", 2, "3"]
// example
// privacyViews -> JSX.Element[]
const privacyViews = joinArray(
privacys.value.map(({ key, name }) => {
return (
<button onClick={() => clickPrivacy(key!)} class={Style.privacyBtn}>
{name}
</button>
);
}),
<span class={Style.privacyBtn}>、</span>
);
This will cover all cases
const items = []
if(x1) {
items.push(<span>text1</span>)
}
if(x2) {
items.push(<span>text3</span>)
}
if(x3) {
items.push(<span>text3</span>)
}
return <div>
<>{items.reduce((result, item) => result.length > 0 ? [...result, ', ', item] : [item], [])}</>
</div>
Related
So, I have this filter already working that implements a List in React (Using Mantine.dev as my CSS template):
<List>
{locations.filter(location => {
const locServices: Service[] = [];
location.services.forEach(service => {
if (service.locationPerformsService) {
locServices.push(service);
}
});
return query === ''
? location
: location.name.toLowerCase().includes(query.toLowerCase()) && location;
}).map((location, index) => (
<UnstyledButton
key={index}
className={
selectedLocation.risCode === location.risCode
? 'selected-location-navbar-item location-navbar-item'
: 'location-navbar-item'
}
>
</UnstyledButton>
))}
</List>
I do have a nested array that I need to verify, but I can't see a better way to implement both together:
locations.forEach(location => {
const locServices: Service[] = [];
location.services.forEach(service => {
if (service.locationPerformsService) {
console.log(locServices);
locServices.push(service);
}
});
if (locServices.length > 0) {
console.log(location.name);
console.log(locServices);
}
});
Can you guys give me a hint or show me how to continue ?
Cheers and thanks!
Can we use filter istead of for each ?
locations.forEach(location => {
const locServices = location.services.filter(service => service.locationPerformsService);
if (locServices.length > 0) {
console.log(location.name);
console.log(locServices);
}
});
I believe that you need to read two arrays, right? So you can use the logic to aggregate, like sets, the same idea from Aman Sandhwani.
arr1 = [{}]
arr2 = [{}]
for(i=0; i<len(arr1); i++) {
for(j=0; j<len(arr2); j++) {
if arr1[i].location == arr2.service.location {
// todo your action
}
}
}
Note: approach from Aman is the same idea but simplified.
The array was right, the issue was with the filter, so I had to change the ternary operator to an old if/else/if to do more verifications and added the second array.
{locations.filter(location => {
const locServices: Service[] = [];
location.services.forEach(service => {
if (service.locationPerformsService) {
locServices.push(service);
}
});
if (query === '') {
return location;
}
else if(location.name.toLowerCase().includes(query.toLowerCase()))
{
return location;
}
else if(locServices.length > 0)
{
return locServices;
}
}).map((location, index) => (
<UnstyledButton
key={index}
className={
selectedLocation.risCode === location.risCode
? 'selected-location-navbar-item location-navbar-item'
: 'location-navbar-item'
}
>
So I've created a quiz-app that fetches 10 random questions with three incorrect and one correct answer to choose from. Everything is working great at the moment except that I can't get the answers to pop up randomly. And by that I mean that the correct answer is always at the bottom of the choices presented.
I know that the answer is Math.floor(Math.random() * ... ) but I honestly have no idea where to put it. I've tried everything. I could really use some help.
import React, { Component } from "react";
import "./App.css";
const API =
"https://opentdb.com/api.php?amount=10&category=20&difficulty=medium";
class App extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
score: []
};
}
componentDidMount() {
this.populateAppWithData();
}
populateAppWithData() {
fetch(API)
.then(response => response.json())
.then(data => this.setState({ results: data.results }))
.catch(error => console.error(error))
}
render() {
const { results } = this.state;
return (
<div className="App">
<h1>Quiz App</h1>
<TheCounter results={results}
Counter={this.state.score}
/>
</div>
);
}
}
class MythologyAnswers extends Component {
constructor(props) {
super(props);
this.state = {
answered: "",
isRight: null,
};
}
answerClicked(answer) {
const { hasAnswered, correct_answer } = this.props;
return event => {
const isRight = correct_answer === answer;
hasAnswered(isRight);
this.setState({
answered: answer,
isRight,
});
};
}
render() {
const { question, correct_answer, incorrect_answers } = this.props;
const { answered, isRight } = this.state;
return (
<div className="allAnswers">
{question}
{incorrect_answers && incorrect_answers
.concat(correct_answer)
.map(answer => (<div onClick={this.answerClicked(answer)}>{answer} </div>))}<br />
{answered && `You answered ${answered}`} {" "} <br />
<div className="correctAnswer"> {" "}{answered && isRight && "This is correct!"} </div> <br />
<div className="incorrectAnswer"> {" "}{answered && !isRight && `This is incorrect. The correct answer is ${this.props.correct_answer}`} {" "}</div>
</div>
)
}
}
class TheCounter extends Component {
constructor(props) {
super(props);
this.state = {
right: 0,
Counter: 0,
unanswered: 0,
};
}
questionAnswered = isRight => {
this.setState(({ Counter, right }) => ({
Counter: Counter + 1,
right: right + isRight,
}));
}
render() {
const { results } = this.props;
const { Counter } = this.state;
const unanswered = this.props.results && Counter;
if (unanswered >= 10) {
return <div className="theUnanswered"> You got {this.state.right} right out of {this.state.Counter} </div>;
}
const question = results[Counter];
return (
<div className="newQuestion">
<MythologyAnswers {...question} hasAnswered={this.questionAnswered} />
</div>
)
}
}
export default App;
We just need to apply the randomizer in the right place. You use .concat() to combine the two arrays, so it makes sense to use the randomizer right after that and before we call .map()
I've set up something that preserves alot of the existing logic you already wrote.
This will help create the new Arr and set-up the markup for your component.
renderChoices = () => {
const { correct_answer, incorrect_answers } = this.props;
let allAnswers = incorrect_answers
? incorrect_answers.concat(correct_answer)
: [];
//simple shuffle algorithm. Just inject your array and it'll pop out a new one.
function createRandom(arr) {
let myArr = [...arr]; //copy arr we pass in
let randomizedArr = []; //gets popuated by loop
while (myArr.length > 0) {
var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
}
//when loop has finished, return random array
return randomizedArr;
}
//call randomizer and get new Arr
let randomizedArr = createRandom(allAnswers);
//use .map to create markup with randomizedArr
return randomizedArr.map(answer => {
return <div onClick={this.answerClicked(answer)}>{answer}</div>;
});
};
So if you were to call the above function inside render, it will create the answer-set for you.
render() {
const { question } = this.props;
const { answered, isRight } = this.state;
return (
<div className="allAnswers">
{question}
{ this.renderChoices()}
<br />
{answered && `You answered ${answered}`} <br />
<div className="correctAnswer">
{answered && isRight && "This is correct!"}
</div>
<br />
<div className="incorrectAnswer">
{answered &&
!isRight &&
`This is incorrect. The correct answer is ${
this.props.correct_answer
}`}
</div>
</div>
);
}
Clean and not too-complex :)
Edit: so without changing your original code too much:
createRandom(arr) {
let myArr = [...arr]; //copy arr we pass in
let randomizedArr = []; //gets popuated by loop
while (myArr.length > 0) {
var randomIndex = Math.floor(Math.random() * myArr.length); //create random number
randomizedArr.push(myArr[randomIndex]); //add choice randomly to arr
myArr.splice(randomIndex, 1); //cut out a piece of the array then resart loop
}
//when loop has finished, return random array
return randomizedArr;
}
render() {
const { question, correct_answer, incorrect_answers } = this.props;
const { answered, isRight } = this.state;
const allAnswers =
incorrect_answers ? incorrect_answers.concat(correct_answer) : [];
const randomizedAnswers = this.createRandom(allAnswers)
return (
<div className="allAnswers">
{question}
{randomizedAnswers
.map(answer => (
<div onClick={this.answerClicked(answer)}>{answer} </div>
))}
<br />
{answered && `You answered ${answered}`} <br />
<div className="correctAnswer">
{answered && isRight && "This is correct!"}
</div>
<br />
<div className="incorrectAnswer">
{answered &&
!isRight &&
`This is incorrect. The correct answer is ${
this.props.correct_answer
}`}
</div>
</div>
);
}
So in the edited version we do a couple things:
Defined a function called createRandom() ... all it does is
randomize your answers choices.
In render, we created a new variable called allAnswers which just
concat() the incorrect_answers and correct_answer like you did
previously. If there are no incorrect_answers defined, we will use
an empty array [] as the default value.
Create a new variable called randomizedAnswers. We call
createRandom() and pass in allAnswers as the argument. It returns
the randomized array for us to use.
Then just .map() over randomizedAnswers to create your
answer-choices markup.
In below code , I suppose you are trying to concat incorrect_answer array with the correct_answer
incorrect_answers && incorrect_answers
.concat(correct_answer)
so array becomes [incorrect_answers,incorrect_answers,incorrect_answers,correct_answer]
i.e correct answer is at end
so if you want to insert correct answer at random position and suppose there are 4 options then first generate a random number between 0 and 3
let randonIndex = Math.floor(Math.random() * 4)
then insert correct answer at random index
incorrect_answers && incorrect_answers
.splice(randomIndex, 0, correct_answer);
I'd personally add it right after fetching the data. Using an inline shuffle like Fisher-Yales you don't need to modifiy your structure at all.
const fisherYalesShuffle = (a) => {
let j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
}
However, since you generally want to move as much data-logic as possible to your backend, you should also consider shuffling them there instead.
You can define a shuffle function (This one is Fisher-Yates (aka Knuth) shuffle):
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
And then just apply the function when setting the state like this:
this.setState({ results: this.shuffle(data.results) })
I'm trying to do something very simple but its not playing well with my code. I can see it render but only 3 times and not 9
const renderTempBoxes = () => {
for (var i = 0; i < 10; i++) {
console.log('i = ', i);
return <div className={styles.box} key={i} />;
}
};
const Component = () => {
return (
{renderTempBoxes()}
)
}
This doesn't even work, which is overkill to use an array when I just want 9 boxes to render.
UPDATE:
const Component = () => {
return (
<div>
{
[...Array(10)].map((x, i) => {
console.log('i = ', i);
return <div className={styles.box} key={i} />;
})
}
</div>
)
}
The first issue is that you simply cannot return individual elements from within the for loop like that. This is not specific to React, this is simply a JavaScript issue. Instead you can try something like this using Array.from to map an array of elements:
const renderTempBoxes = () => Array.from({ length: 10 }).map((v, i) =>
<div className={styles.box} key={i}>{i}</div>
);
Or simply the for loop with Array.prototype.push to generate an array of elements and return it:
const renderTempBoxes = () => {
let els = [];
for (let i = 0; i < 10; i++) {
els.push(<div className={styles.box} key={i}>{i}</div>);
}
return els;
};
Rendering the elements:
const Component = () => {
return (
<div>
{renderTempBoxes()}
</div>
)
}
Or with React.Fragment to forgo the wrapping extra node:
const Component = () => {
return (
<React.Fragment>
{renderTempBoxes()}
</React.Fragment>
)
}
The second issue with your example is that <div /> isn't going to really render anything, it's not a void/self-closing element such as <meta />. Instead you would need to do return the div element as <div className={styles.box} key={i}>{whatever}</div>.
Regarding the syntax [...Array(10)], there must be an Webpack in terms of how it handles/transpiles Array(10), [...Array(10)], [...new Array(10)], or even `[...new Array(10).keys()]. Either of the approaches described in the answer should solve your issue.
I've created a StackBlitz to demonstrate the functionality.
When trying to render multiple times the same components use an array an map over it.
export default class MyComp extends Component {
constructor(props) {
super(props)
this.state = {
array: [{key: 1, props: {...}}, {key: 2, props: {...}, ...]
}
}
render () {
return (
<div>
{this.state.array.map((element) => {
return <div key={element.key} {...element.props}>
})}
</div>
)
}
}
Remember to always set a unique key to every component you render
I'm trying to replace parts of a string with JSX tags, like so:
render: function() {
result = this.props.text.replace(":",<div className="spacer"></div>);
return (
<div>
{result}
<div>
);
}
But given that this.props.text is Lorem : ipsum, it results in
<div>
Lorem [object Object] ipsum.
</div>
Is there a way to solve this or another way to replace parts of a string with JSX tags?
The accepted answer worked well in 2015, but now better solutions exist.
For this problem issue #3368 was created and based on the solution provided by a Facebook employee working on React, react-string-replace was created.
Using react-string-replace, here is how you can solve your problem
const reactStringReplace = require('react-string-replace');
const HighlightNumbers = React.createClass({
render() {
const content = 'Hey my number is 555:555:5555.';
return (
<span>
{reactStringReplace(content, ':', (match, i) => (
<div className="spacer"></div>
))}
</span>
);
},
});
When you pass a JSX element to replace() as the second argument, that element is converted to a string because replace() expects a string as a second argument. What you need to do is convert your string to an array of strings and JSX elements. So your result variable should contain something like ['Lorem ', <div className="spacer"></div>, ' ipsum'].
Something like this:
function flatMap(array, fn) {
var result = [];
for (var i = 0; i < array.length; i++) {
var mapping = fn(array[i]);
result = result.concat(mapping);
}
return result;
}
var Comp = React.createClass({
render: function () {
var result = 'Lorem : ipsum';
result = flatMap(result.split(':'), function (part) {
return [part, <div>spacer</div>];
});
// Remove the last spacer
result.pop();
return (
<div>
{result}
</div>
);
}
});
As I am a perfectionist. I think this pattern is perfect:
String.prototype.replaceJSX = (find, replace) => {
return this.split(find).flatMap((item) => [item, replace]);
}
Usage:
variable.replaceJSX(":", <br />);
Wrote a utility function for jsx.
const wrapTags = (text: string, regex: RegExp, className?: string) => {
const textArray = text.split(regex);
return textArray.map(str => {
if (regex.test(str)) {
return <span className={className}>{str}</span>;
}
return str;
});
};
The following should also work (assumes ES6), The only nuance is that the text is actually wrapped inside a DIV element and not preceded by it, assuming you are going to use CSS for the actual spacing this shouldn't be a problem.
const result = this.props.text.split(':').map(t => {
return <div className='textItem'>{t}</div>;
});
I have come to following simple solution that does not include third party library or regex, maybe it can still help someone.
Mainly just use .replace() to replace string with regular html written as string, like:
text.replace('string-to-replace', '<span class="something"></span>')
And then render it using dangerouslySetInnerHTML inside an element.
Full example:
const textToRepace = 'Insert :' // we will replace : with div spacer
const content = textToRepace.replace(':', '<div class="spacer"></div>') : ''
// then in rendering just use dangerouslySetInnerHTML
render() {
return(
<div dangerouslySetInnerHTML={{
__html: content
}} />
)
}
I just a wrote function helper to replace some Texts, Components, and even HTMLs to a template string for my project. It turned out so nice and smooth.
const replaceJSX = (str, replacement) => {
const result = [];
const keys = Object.keys(replacement);
const getRegExp = () => {
const regexp = [];
keys.forEach((key) => regexp.push(`{${key}}`));
return new RegExp(regexp.join('|'));
};
str.split(getRegExp()).forEach((item, i) => {
result.push(item, replacement[keys[i]]);
});
return result;
};
Usage:
const User = ({ href, name }) => {
return (
<a href={href} title={name}>
{name}
</a>
);
};
const string = 'Hello {component}, {html}, {string}';
return (
<div>
{replaceJSX(string, {
component: (
<User
href='https://stackoverflow.com/users/64730/magnus-engdal'
name='Magnus Engdal'
/>
),
html: (
<span style={{ fontWeight: 'bold' }}>
This would be your solution
</span>
),
string: 'Enjoy!',
})}
</div>
)
And you'll get something like this:
<div>Hello Magnus Engdal, <span style="font-weight: bold;">This would be your solution.</span>, Enjoy!.</div>
After some research I found that existing libraries doesn't fit my requirements. So, of course, I have written my own:
https://github.com/EfogDev/react-process-string
It is very easy to use. Your case example:
let result = processString({
regex: /:/gim,
fn: () => <div className="spacer"></div>
})(this.props.test);
I had the more common task - wrap all (English) words by custom tag.
My solution:
class WrapWords extends React.Component {
render() {
const text = this.props.text;
const isEnglishWord = /\b([-'a-z]+)\b/ig;
const CustomWordTag = 'word';
const byWords = text.split(isEnglishWord);
return (
<div>
{
byWords.map(word => {
if (word.match(isEnglishWord)) {
return <CustomWordTag>{word}</CustomWordTag>;
}
return word;
})
}
</div>
);
}
}
// Render it
ReactDOM.render(
<WrapWords text="Argentina, were playing: England in the quarter-finals (the 1986 World Cup in Mexico). In the 52nd minute the Argentinian captain, Diego Maradona, scored a goal." />,
document.getElementById("react")
);
<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="react"></div>
Nothing around WEB globe worked for me exept this solution - https://www.npmjs.com/package/regexify-string
Working with React, strings and doesn't have any additional dependecies
regexifyString({
pattern: /\[.*?\]/gim,
decorator: (match, index) => {
return (
<Link
to={SOME_ROUTE}
onClick={onClick}
>
{match}
</Link>
);
},
input: 'Some text with [link]',
});
Something like this:
function replaceByComponent(string, component) {
const variable = string.substring(
string.lastIndexOf("{{") + 2,
string.lastIndexOf("}}")
);
const strParts = string.split(`{{${variable}}}`);
const strComponent = strParts.map((strPart, index) => {
if(index === strParts.length - 1) {
return strPart
}
return (
<>
{strPart}
<span>
{component}
</span>
</>
)
})
return strComponent
}
In my case, I use React and I wanted to replace url in text with anchor tag.
In my solution, I used two library.
simple-text-parser
url-regex
and wrote this code.
/* eslint-disable react/no-danger */
import React from 'react';
import { Parser } from 'simple-text-parser';
import urlRegex from 'url-regex';
type TextRendererProps = { text: string };
const parser = new Parser();
const re = urlRegex();
parser.addRule(re, (url) => {
return `${url}`;
});
export const TextRenderer: React.FC<TextRendererProps> = ({ text }) => {
return (
<span
dangerouslySetInnerHTML={{
__html: parser.render(text),
}}
/>
);
};
You can easily add replacing rule by just writing parser.addRule().
Adding recursiveness to #Amir Fo's answer:
export const replaceJSX = (subject, find, replace) => {
const result = [];
if(Array.isArray(subject)){
for(let part of subject)
result = [ ...result, replaceJSX(part, find, replace) ]
return result;
}else if(typeof subject !== 'string')
return subject;
let parts = subject.split(find);
for(let i = 0; i < parts.length; i++) {
result.push(parts[i]);
if((i+1) !== parts.length)
result.push(replace);
}
return result;
}
export const replaceJSXRecursive = (subject, replacements) => {
for(let key in replacements){
subject = replaceJSX(subject, key, replacements[key])
}
return subject;
}
You can now replace any number of strings with JSX elements at once by calling replaceJSXRecursive like below:
replaceJSXRecursive(textVar, {
':': <div class="spacer"></div>,
';': <div class="spacer2"></div>
})
If you'd also like to be able to make replacements within replacements (for example, to highlight search terms within urls), check out this node module I created - https://github.com/marcellosachs/react-string-replace-recursively
Example with hooks:
import React, { useEffect, useState, useRef } from 'react'
export function Highlight({ value, highlightText }) {
const [result, resultSet] = useState(wrap())
const isFirstRun = useRef(true)
function wrap() {
let reg = new RegExp('(' + highlightText + ')', 'gi')
let parts = value.split(reg)
for (let i = 1; i < parts.length; i += 2) {
parts[i] = (
<span className='highlight' key={i}>
{parts[i]}
</span>
)
}
return <div>{parts}</div>
}
useEffect(() => {
//skip first run
if (isFirstRun.current) {
isFirstRun.current = false
return
}
resultSet(wrap())
}, [value, highlightText])
return result
}
i think this is the most light perfect solution:
render() {
const searchValue = "an";
const searchWordRegex = new RegExp(searchValue, "gi");
const text =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text";
return (
<div>
{text.split(searchWordRegex).length > 1
? text.split(searchWordRegex).map((chunk, index) => {
if (chunk !== "") {
return index === 0 &&
! new RegExp("^" + searchValue, "gi").test(text) ? (
chunk
) : (
<span key={index}>
<span
className="highlight"
style={{
fontWeight: "bold"
}}
>
{searchValue.charAt(0).toUpperCase() +
searchValue.slice(1)}
</span>
{chunk}
</span>
);
} else {
return null;
}
})
: text}
</div>
);
}
and here is a working example
1.use regex capture the words you want
2.str.spilt(regex)
eg:
var string4 = 'one split two splat three splot four';
var splitString4 = string4.split(/\ (split|splat|splot)\ /);
console.log(splitString4); // Outputs ["one", "split", "two", "splat", "three", "splot", "four"]
3.render array
arr.map(i=> <div>{i}</div>)
This is mostly similar to Spleen's answer, however I'm unable to edit it (edit queue full) so posting it as a separate answer.
The key differences with the below is that it's been structured towards Typescript and more importantly it won't embed the replacement value at the end of the string if it cannot be found.
export const replaceWithJsx = (
valueToUpdate: string,
replacement: Record<string, JSX.Element>,
) => {
const result: Array<string | JSX.Element> = [];
const keys = Object.keys(replacement);
const regExp = new RegExp(keys.join("|"));
valueToUpdate.split(regExp).forEach((item, index) => {
result.push(item);
const key = keys[index];
if (valueToUpdate.includes(key)) {
result.push(replacement[key]);
}
});
return result;
};
I'm trying to output a list of comma separated links and this is my solution.
var Item = React.createComponent({
render: function() {
var tags = [],
tag;
for (var i = 0, l = item.tags.length; i < l; i++) {
if (i === item.tags.length - 1) {
tag = <span><Tag key={i} tag={item.tags[i]} /></span>;
} else {
tag = <span><Tag key={i} tag={item.tags[i]} /><span>, </span></span>;
}
tags.push(tag);
}
return (
<tr>
<td>
{item.name}
</td>
<td>
{tags}
</td>
</tr>
);
}
});
I was just wondering if there was a better, more clean way to accomplish this?
Thanks
Simply
{tags.map((tag, i) => <span key={i}>
{i > 0 && ", "}
<Tag tag={tag} />
</span>)}
In React 16 it can be done even more simpler:
{tags.map((tag, i) => [
i > 0 && ", ",
<Tag key={i} tag={tag} />
])}
At Khan Academy we use a helper called intersperse for this:
/* intersperse: Return an array with the separator interspersed between
* each element of the input array.
*
* > _([1,2,3]).intersperse(0)
* [1,0,2,0,3]
*/
function intersperse(arr, sep) {
if (arr.length === 0) {
return [];
}
return arr.slice(1).reduce(function(xs, x, i) {
return xs.concat([sep, x]);
}, [arr[0]]);
}
which allows you to write code like:
var tags = item.tags.map(function(tag, i) {
return <Tag key={i} tag={item.tags[i]} />;
};
tags = intersperse(tags, ", ");
Or simply write the list items to an unordered list and use CSS.
var Item = React.createComponent({
render: function() {
var tags = this.props.item.tags.map(function(i, item) {
return <li><Tag key={i} tag={item} /></li>
});
return (
<tr>
<td>
{this.props.item.name}
</td>
<td>
<ul className="list--tags">
{tags}
</ul>
</td>
</tr>
);
}
});
And the CSS:
.list--tags {
padding-left: 0;
text-transform: capitalize;
}
.list--tags > li {
display: inline;
}
.list--tags > li:before {
content:',\0000a0'; /* Non-breaking space */
}
.list--tags > li:first-child:before {
content: normal;
}
import React from 'react';
import { compact } from 'lodash';
// Whatever you want to separate your items with commas, space, border...
const Separator = () => { ... };
// Helpful component to wrap items that should be separated
const WithSeparators = ({ children, ...props }) => {
// _.compact will remove falsey values: useful when doing conditional rendering
const array = compact(React.Children.toArray(children));
return array.map((childrenItem, i) => (
<React.Fragment key={`${i}`}>
{i > 0 && <Separator {...props} />}
{childrenItem}
</React.Fragment>
));
};
const MyPage = () => (
<WithSeparators/>
<div>First</div>
{second && (<div>Maybe second</div>)}
{third && (<div>Maybe third</div>)}
<div>Fourth</div>
</WithSeparators>
);
A function component that does the trick. Inspired by #imos's response. Works for React 16.
const Separate = ({ items, render, separator = ', ' }) =>
items.map((item, index) =>
[index > 0 && separator, render(item)]
)
<Separate
items={['Foo', 'Bar']}
render={item => <Tag tag={item} />}
/>
Here's a solution that allows <span>s and <br>s and junk as the separator:
const createFragment = require('react-addons-create-fragment');
function joinElements(arr,sep=<br/>) {
let frag = {};
for(let i=0,add=false;;++i) {
if(add) {
frag[`sep-${i}`] = sep;
}
if(i >= arr.length) {
break;
}
if(add = !!arr[i]) {
frag[`el-${i}`] = arr[i];
}
}
return createFragment(frag);
}
It filters out falsey array elements too. I used this for formatting addresses, where some address fields are not filled out.
It uses fragments to avoid the warnings about missing keys.
Simple one:
{items.map((item, index) => (
<span key={item.id}>
{item.id}
{index < items.length - 1 && ', '}
</span>
))}
To add to the great answers above Ramda has intersperse.
To comma separate a bunch of items you could do:
const makeLinks = (x: Result[]) =>
intersperse(<>,</>, map(makeLink, x))
Pretty succinct
The solution without extra tags
<p className="conceps inline list">
{lesson.concepts.flatMap((concept, i) =>
[concept, <span key={i} className="separator">•</span>]
, ).slice(-1)}
</p>
generates something like
Function • Function type • Higher-order function • Partial application
The easiest way to do
const elementsArr = ["a", "b", "c"];
let elementsToRender = [] ;
elementsArr.forEach((element, index) => {
let elementComponent = <TAG className="abc" key={element.id}>{element}</TAG>
elementsToRender.push(elementComponent);
if(index !== (elementsArr.length - 1)){
elementsToRender.push(", ");
}
});
render(){
return (
<div>{elementsToRender}</div>
)
}