问题
I have a string , i.e,
let string= "Hello <b>Click here</b>";
render() {
return (<div dangerouslySetInnerHTML={this.createMarkup(value)}/>
}
createMarkup = value => {
return { __html: value };
};
What I would like is to be able to add an onclick event to the <b>
tag perform state manipulations on click.
The underlying problem is where I had a function which was supposed to render whatever is passed by the API. The API would send a string 'Money received for order ID 123', or could be any string that I have no control over. Later, I got a requirement where the item that is bolded must be clickable, so as to perform some actions. I didn't have any other way to solve it.
How can I achieve this?
回答1:
Caveat: This sounds like an X/Y problem, where the underlying problem (whatever it is) should be solved differently, so that you don't have to add a click handler to a DOM element created via (You've clarified the use case; solution #1 below applies and isn't poor practice.)dangerouslySetInnerHTML
(ideally, so you don't have to create DOM elements via dangerouslySetInnerHTML
at all). But answering the question you asked:
I don't think you can do that directly. Two solutions I can think of:
Use delegated event handler on the
div
: Add a click handler on thediv
, but then only take action if the click passed through theb
element.Use a
ref
on thediv
, and then hook the click handler up incomponentDidMount
andcomponentDidUpdate
(finding theb
element within thediv
viaquerySelector
or similar), something along these lines:
Here's an example of #1:
<div onClick={this.clickHandler} dangerouslySetInnerHTML={this.createMarkup(string)}/>
...where clickHandler
is
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
const el = e.target.closest("B");
if (el && e.currentTarget.contains(el)) {
// ...do your state change...
}
}
...or if you need to support older browsers without ParentNode#closest
:
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
let el = e.target;
while (el && el !== e.currentTarget && el.tagName !== "B") {
el = el.parentNode;
}
if (el && el.tagName === "B") {
// ...do your state change...
}
}
...and where you bind clickHandler
in the constructor (rather than using a property with an arrow function; why: 1, 2):
this.clickHandler = this.clickHandler.bind(this);
Live Example:
let string = "Hello <b>Click here</b>";
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
// Version supporting older browsers:
let el = e.target;
while (el && el !== e.currentTarget && el.tagName !== "B") {
el = el.parentNode;
}
if (el && el.tagName === "B") {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
// Alternative for modern browsers:
/*
const el = e.target.closest("B");
if (el && e.currentTarget.contains(el)) {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
*/
}
createMarkup = value => {
return { __html: value };
};
render() {
const {clicks} = this.state;
return [
<div>Clicks: {clicks}</div>,
<div onClick={this.clickHandler} dangerouslySetInnerHTML={this.createMarkup(string)}/>
];
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Here's an example of #2, but don't do this if A) You can solve the underlying problem separately, or B) #1 works:
let string = "Hello <b>Click here</b>";
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
this.divRef = React.createRef();
this.hooked = null;
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler() {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
hookDivContents() {
// Get the b element
const b = this.divRef.current && this.divRef.current.querySelector("b");
// No-op if it's not there or it's the same element we have hooked
if (!b || b === this.hooked) {
return;
}
// Unhook the old, hook the new
if (this.hooked) {
this.hooked.removeEventListener("click", this.clickHandler);
}
this.hooked = this.divRef.current;
this.hooked.addEventListener("click", this.clickHandler);
}
componentDidMount() {
this.hookDivContents();
}
componentDidUpdate() {
this.hookDivContents();
}
createMarkup = value => {
return { __html: value };
};
render() {
const {clicks} = this.state;
return [
<div>Clicks: {clicks}</div>,
<div ref={this.divRef} dangerouslySetInnerHTML={this.createMarkup(string)}/>
];
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Refs are an "escape hatch" giving you direct DOM access. Don't use refs lightly; usually, there's a better choice.
But again: I would solve the underlying problem, whatever it is, differently.
回答2:
react-html-parser can convert HTML strings into React components.
using transform callback function you can update any tag in HTML string with JSX tag adding any properties and event listeners.
This is how I used it:
ReactHtmlParser(item.value, {
transform: (node) => {
if (node.name === 'a' && node.attribs && node.attribs.href) {
const matched = node.attribs.href.match(/^activity\/([0-9]+)$/i);
if (matched && matched[1]) { // activity id
return <a
href={node.attribs.href}
onClick={(e) => {
e.preventDefault();
this.props.openActivityModal(matched[1]);
}}
>{node.children[0].data}</a>
}
}
}
})
回答3:
Here is a clean way to achieve your needs. By splitting your string depending on the <br>
tag you can end up with an mappable array of text :
class BoldText extends React.Component {
constructor(props) {
super(props)
this.state = {
input: "Money received for order ID <b>123</b>, wow for real, so <b>cool</b> its insane"
}
}
boldClick = ev => {
console.log('clicked !')
}
render() {
const { input } = this.state
const a = input.split('</b>')
const filter = /<b>.*<\/b>/
const text = input.split(filter)
const clickable = filter.exec(input)
//<b onClick={this.boldClick}></b>
return (
<div>
<p>{a.map(t => {
const [text, bold] = t.split('<b>')
return <span>{text}<b onClick={this.boldClick}>{bold}</b></span>
})}
</p>
</div>
)
}
}
ReactDOM.render(<BoldText />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<idv id='root'>
This solution should solve the problem you mentioned in the comments of the answer above. You can put your API call in the componentDidMount
lifecycle function and change your state from there.
来源:https://stackoverflow.com/questions/54109790/how-to-add-onclick-event-to-a-string-rendered-by-dangerouslysetinnerhtml-in-reac