I want to concatenate two strings in react such that the first displays bold and the second does not. I have the string I want bolded in a JSON file and I have the string I want to concatenate coming from backend API call. Here's my setup:
This is in a JSON file:
{ stuff: {
stuffIWantBolded: "bold text"
}
}
And my frontend looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const textIWantToDisplay = `${stuff.stuffIWantBolded} ${theStringFromBackend}`;
return (
<SomeComponent someProp={textIWantToDisplay} />
);
};
That concatenates the two strings successfully. I've tried using .bold() at the end of stuff.stuffIWantBolded, but that apparently doesn't work, because then the string renders as <b>bold text</b> the string from backend (assuming that's what the string from backend says), with the HTML tags written out explicitly instead of rendering actual bold text. Is there something I'm missing? I don't think one can just make the string bold in the JSON...perhaps I need to use a regex? Any help would be greatly appreciated.
How about this:
return (
<>
<span style={{fontWeight: "bold"}}>{stuff.stuffIWantBolded}</span>
<span>{theStringFromBackend}</span>
</>
);
The <> and </> effectively allow you to return multiple items from one render function.
I would do it by using composition (I prefer the strong tag to b to make text bold):
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
return (
<SomeComponent>
<p><strong>{stuff.stuffIWantBolded}</strong> {theStringFromBackend}</p>
</SomeComponent>
);
};
And in SomeComponent you will find the nested html inside props.children
for more info check here: https://reactjs.org/docs/composition-vs-inheritance.html
It turns out you CAN pass in a prop within a self-closing tag component the way I want to. Not sure if it's conventionally sound/overly bloated but it looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const theBoldedText = `${stuff.stuffIWantBolded}`;
return (
<SomeComponent someProp={<span><b>{theBoldedText}</b> <span>{theStringFromBackend}</span></span>} />
);
};
Related
What I'm trying to do is to get HTML tag by className on dynamic HTML that I fetched,
but it returns undefined. It works if I try to getElementByClassName("main-page") because that class isn't dynamic
const HomePage = () => {
const [pageData, setPageData] = useContext(PageContext)
useEffect(() => {
const allImages = document.getElementsByClassName("wp-block-column")
console.log([...allImages])
}, [])
//render fronpage
const renderMainPage = () => {
//map the data and check for the site url (www.siteurl.com = front page)
if (pageData) {
return pageData.map(page => {
if (window.location.origin + "/" === page.link) {
return <section dangerouslySetInnerHTML={{ __html: page.content.rendered }}></section>
}
})
}
}
return (
<h1 className="main-page"">{renderMainPage()}</h1>
)
}
export default HomePage
From what I see the problem in pageData in context, probably your default value to context's pageData is false and renderMainPage() is not going further then first "if" statement
But its not preferred to use document selectors, use refs in React instead.
Also from naming I see that you are trying to get images and not dom nodes, but with this logic you are going to get dom nodes. I'm sure there is better way/flow to access images you need than looking for them in nodes.
const elementsRef = useRef(data.map(() => createRef()));
you can you dynamic create ref to access or create random iniNumber Array then assing to you elements when select them
You can use this function that useLayoutEffect, you can modify your code like this,
useLayoutEffect(()=> {
document.getElementsByClassName("yourclassname")
})
In my react electron app, that it is working with an API, I receive JSON values to display data into the components. So for example I have a Features component:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="${() => shell.openExternal('www.google.com')}"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
What I want is to replace the href attribute with onClick and assign Electron's shell.openExternal() function.
The string.replace() callback function does that, but when I click on the <a> element, the app throws next error:
error: uncaughtException: Expected onClick listener to be a
function, instead got a value of string type.
UPDATE
Also tried this logic and the same error occurs:
global.openInBrowser = openInBrowser; // openInBrowser is basically a function that calls shell.openExternal(url)
const re = new RegExp('<a([^>]* )href="([^"]+)"', 'g');
string = string.replace(re, '<a$1href="#" onClick="openInBrowser(\'$2\')"');
Here's a link to Sandbox rep.
How do I do this correctly?
The onclick not being set on the React element is actually expected behavior.
Because there's an XSS security risk when evaling the onclick string. The recommended solution is to use the replace option in html-react-parser.
you can also use dangerouslySetInnerHTML which involves security risk.
Sandbox Demo
export default function App() {
let string =
'This is a string containing html link to Google';
const re = new RegExp('<a([^>]* )href="([^"]+)"', "g");
let replaced = string.replace(re, "<a onclick=\"alert('$2')\"");
return (
<div className="App">
<p dangerouslySetInnerHTML={{__html: replaced}}></p>
<p>{parse(string, {
replace: domNode => {
if (domNode.name === 'a') {
let href = domNode.attribs.href
domNode.attribs.onClick = () => { alert(href) }
delete domNode.attribs.href
}
}
})}</p>
</div>
);
}
Not sure on specifics of electron, but passing a (function(){functionName()})() would not work in html if there is no functionName variable available on window scope. Being there is a global environment in electron this might answer your question:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
function runOpen(href){
shell.openExternal(href)
}
global.runOpen = runOpen;
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="runOpen(${'www.google.com'})"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
if it doesnt you can use something like onclick="console.log(this)" to find out what is the scope the onclick runs in and futher assign your runOpen variable there.
I have the following JSON Data coming out of Firebase RTDB:
{"\"1\"":{"itemDescription":"This is a description of the item.","itemName":"Item Name","itemValue":{"costAmount":200,"costType":"dol"}},
"\"2\"":{"itemDescription":"This is an item description.","itemName":"Item Name 2","itemValue":{"costAmount":500,"costType":"dol"}}}
and so on...
The data is parsed (json.parse) and stored a variable called parsedJSONData in state.
I've tried looping through it using other recommendations on this site and others, such as:
this.state.parsedJSONData.map(json => {
<div className="item">
<p>Item: {json.itemName}</p>
</div>;
});
And I'm getting errors like the below:
Line 68: Expected an assignment or function call and instead saw an expression no-unused-expressions
Any tips on what I can do? I know the data is parsed correctly, because if I output the contents of the parsedJsonData to the console, I can see the data is structured correctly?
Try Using forEach() instead of map()
You are not returning anything inside of your map(). Here you can look at the different ways to return values from an arrow function, but in short, data will be returned in these two forms:
json => json.itemName
json => {return json.itemName;}
Drop the {} inside of your map or throw a return inside like so:
this.state.parsedJSONData.map(({itemName}) =>
<div className="item">
<p>Item: {itemName}</p>
</div>
);
or
this.state.parsedJSONData.map(({itemName}) => {
return <div className="item">
<p>Item: {itemName}</p>
</div>;
});
Try this. I have tried this and it works for the your json Data.
render() {
var tempData = [];
const theDataWeHave = this.state.parsedJSONData;
for(var row in theDataWeHave){
var rowEntry = theDataWeHave[row];
tempData.push(rowEntry);
}
var renderVariable = tempData.map(function(item,index) {
return ( <RenderThisComponent key={index} item={item}/> )
})
}
return(
//your code for other stuff
{renderVariable} //Place this where you want to render the component
)
The component you want to render will be a separate functional component with props passed to it.
You can write in the same file or you can write in separate file and import+export
.I have done in the same file so it do not need to be exported or imported.
const RenderThisComponent = (props) => (
<div className="item">
<p>Item: {props.item.itemName}</p>
</div>
)
There is problem with render gallery component:
I get string with html from server
let serverResponse = `
<h3>Some title</h3>
<p>Some text</p>
<p>
<img src="">
<img src="">
<img src="">
<br>
</p>
...
`
Now I render this response with dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: serverResponse }} />
But when I got 2 or more repeating <img> tags I want to replace them with component.
How can I do that? I tried to do it with Regex and replace them with <Gallery/> but it doesn't work. I think that I need split string in array of tags and then replace images with <Gallery/> component.
I tried do it with renderToString
...
getGallery = images => {
// Loop throw images and get sources
let sources = [];
if (images) {
images.map(img => {
let separatedImages = img.match(/<img (.*?)>/g);
separatedImages.map(item => sources.push(...item.match(/(https?:\/\/.*\.(?:png|jpg))/)));
});
}
if (sources.length) {
return <Gallery items={sources}>
}
return <div/>
};
...
<div dangerouslySetInnerHTML={{__html: serverResponse.replace(/(<img (.*?)>){2,}/g,
renderToString(this.getGallery(serverResponse.match(/(<img (.*?)>){2,}/g))))}}/>}
And this doesn't work, because I get just html without logic :(
Fist of all, dangerouslySetInnerHTML is not the way to go, you can't insert gallery into in it and have it processed by React. What you need to do is multistep procedure.
1. Parse HTML into document. In this stage you will convert string to valid DOM document. This is very easy to do with DOMParser:
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
I make this helper function to return container with your HTML nodes. It will be need in the next step.
2. Transform DOM document into React JSX tree. Now that you have DOM tree it's very easy to convert it to JSX by creating individual React elements out of corresponding DOM nodes. This function needs to be recursive to process all levels of the DOM tree. Something like this will do:
function getJSX(root) {
return [...root.children].map(element => {
const children = element.children.length ? getJSX(element) : element.textContent
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
This is enough to create JSX out of DOM. It could be used like this:
const JSX = getJSX(getDOM(htmlString))
3. Inject Gallery. Now you can improve JSX creation to inject Gallery into created JSX if element contains more then 1 image tag. I would pass inject function into getJSX as the second parameter. The only difference from above version would be is how children is calculated in gallery case:
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
4. Create Gallery component. Now it's time to create Gallery component itself. This component will look like this:
import React from 'react'
import { func, string } from 'prop-types'
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
function getJSX(root, injectGallery) {
return [...root.children].map(element => {
let children
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
const HTMLContent = ({ content, injectGallery }) => getJSX(getDOM(content), injectGallery)
HTMLContent.propTypes = {
content: string.isRequired,
injectGallery: func.isRequired,
}
export default HTMLContent
5. Use it! Here is how you would use all together:
<HTMLContent
content={serverResponse}
injectGallery={(images) => (
<Gallery images={images} />
)}
/>
Here is the demo of this code above.
Demo: https://codesandbox.io/s/2w436j98n
TLDR: You can use React HTML Parser or similar libraries.
While it looks very alike, JSX get parsed into a bunch of React.createElement so interpolate React component into HTML string will not work. renderToString won't do too because it is used to server-side render React page and will not work in your case.
To replace HTML tags with React component, you need a parser to parse the HTMl string to nodes, map the nodes to React elements and render them. Lucky for you, there are some libraries out there that do just that, like React HTML Parser for example.
I am trying to use the sweetalert-react package (https://github.com/chentsulin/sweetalert-react) as a modal for my app.
Right now I got it to work, but I want to be able to display a const that has my component:
const clusterDoors = lock.doors.map(clusterDoor => {
return (
<div key={clusterDoor.port_id}>
<ClusterListItem
clusterDoor={clusterDoor}
customer={
clusterDoor.allocation.customer ? (
keyedCustomers[clusterDoor.allocation.customer]
) : (
false
)
}
.....
So I read their docs and found out that I can achieve that with ReactDOMServer.renderToStaticMarkup. So I simple need to:
text={renderToStaticMarkup(<MyComponent />)}
But the problem is that my component is inside of a constant, so if I try to do:
text={renderToStaticMarkup({clusterDoors})}
I will get the error:
You must pass a valid ReactElement.
I wonder if there's some workaround to this?
I did some research and tried also tried to do:
const clusterDoors = React.createClass({
render: function() {
lock.doors.map(clusterDoor => {
return (
<div key={clusterDoor.port_id}>
<ClusterListItem
clusterDoor={clusterDoor}
customer={
clusterDoor.allocation.customer ? (
keyedCustomers[clusterDoor.allocation.customer]
) : (
false
)
}
delivery={
clusterDoor.allocation.delivery ? (
keyedDeliveries[clusterDoor.allocation.delivery]
) : (
false
)
}
/>
</div>
)
})
}
})
But that didn't do the trick.
If I pass it a valid react component (ClusterListItem) my app won't break, but nothing will show because of the array clusterDoor wont exist.
I hope I explained my situation well. Thanks for reading.
The problem with your code is that you are passing an array of elements because clusterDoors is an array and renderToStaticMarkup is expecting a single element. Therefore you are getting this error.
Solution: Just wrap your array in a div tag so it becomes a single node element like this
text={renderToStaticMarkup(<div>{clusterDoors}</div>)}