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?
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 dangerouslySetInnerHTML (ideally, so you don't have to create DOM elements via dangerouslySetInnerHTML at all). But answering the question you asked: (You've clarified the use case; solution #1 below applies and isn't poor practice.)
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 the div, but then only take action if the click passed through the b element.
Use a ref on the div, and then hook the click handler up in componentDidMount and componentDidUpdate (finding the b element within the div via querySelector 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.
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>
}
}
}
})
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.
You can make the parent tag a <form> and set the onClick="SomeFunction()".
From the child tag that has the HTML string, set type="button".
let string= "Hello <b type='button'>Click here</b>";
render() {
return (
<form onClick={SomeFunction} dangerouslySetInnerHTML =
{this.createMarkup(value)}/>
}
createMarkup = value => {
return { __html: value };
};
Related
Basically, the code snippet below is for the Close/Edit icon: Once clicked in "Close" mode, it will change to the "Edit" icon and pass the rowId and a "1" parameter in the handleEdit function; and, once in "Edit" mode, it will pass the rowId and a "0" parameter.
The problem is that it only goes to the condition of editClose === 1, although it updates the editDeleteTag to 1; it never seems to render the stylesEditOptions icon along with it's condition.
I'm new to React, so I might be missing something here.
childComponenent.jsx
funcEdit = (editClose) => {
if (editClose === 0) {
return (<div className={styles.editOptions}>
<Input type="button" className={styles.closeIcon} onClick={() => this.props.handleEdit(rowIndex, 1)} />
</div >)
} else {
return (<div className={styles.editOptions}>
<Input type="button" className={styles.EditIcon} onClick={() => this.props.handleEdit(rowIndex, 0)} />
</div >)
}
}
render()
let locArr = [...this.state.mainArray];
For looop .... {
if (locArr[i].editOrDeleteTag === 0) {
locArr[i].editOrDelete = this.funcEdit(1);
} else {
locArr[i].editOrDelete = this.funcEdit(0);
}
}
return(
...
<BootstrapTable data={locArr}
...
)
parentComponent.jsx
handleEdit = (rowId, toggle) => {
let locArr = [...this.state.mainArray];
locArr[rowId.rowIndex].editOrDeleteTag = toggle
this.setState({ mainArray : locArr });
};
The most likely reason is the fact that you are mutating the state.
When making state changes to a nested object you need to update all parent elements.
So in your handleEdit try to use
locArr[rowId.rowIndex] = {
...locArr[rowId.rowIndex],
editOrDeleteTag: toggle
};
instead of
locArr[rowId.rowIndex].editOrDeleuteTag = toggle;
So I'm pretty new to React (and JS) in general. Normally code with Java or C# and I'm not used to the syntax just yet. I did manage to make a simple calculator with React and wanna add the functionality but whenever I try, I get an error that says that my method is not defined.
Basically the function is used to get the prop from my Button component and append it into the Screen component value.
class Button extends React.Component {
constructor(props) {
super(props)
}
add(button) {
let value = button.value
let text = document.getElementByID('screenInput').value
if (value == '=') {
document.getElementById('screenInput').innerHTML = eval(text).toString();
} else if (value == 'C') {
document.getElementById('screenInput').innerHTML = '0':
} else if (value == '+/-' && text != '0') {
document.getElementById('screenInput').innerHTML = `` - (${ text })``;
} else if (value == '\d' || (text.slice(-1) != value && text != '0')) {
document.getElementById('screenInput').innerHTML = text.append(value);
}
}
render() {
return (
<button type='button' value={this.props.value} onClick={() => add(this)}> {this.props.value}</button >
)
}
}
https://jsfiddle.net/q7yakt1v/22/`
So the reason you can't just refer to add like that is it intrinsically bound to the instance of your class Button. So if you want to call it, you'd have to do this.add(this).
The problem you will run into next is that the this you are passing to add isn't the button element, but a reference to the class instance.
I guess I should also add that you don't want to be modifying the DOM directly outside of React like that because React internally keeps track of changes. You should prefer using state/context to apply changes to things outside the immediate scope your component.
As pointed out in a couple of the other answers, the key issue with your code is onClick={() => add(this)}. But I think this is because of a misunderstanding of how state is handled in React.
I have lifted the state of the value of screen into the <Calculator /> component. This way we pass the state into the <Screen /> component to display it and can manipulate it from the <Buttons /> component without calling the DOM directly (as #Avanthika pointed out in their answer).
class Calculator extends React.Component {
constructor(props) {
super(props)
this.state = {
value: '0'
}
this.handleButtonClick = this.handleButtonClick.bind(this)
}
handleButtonClick(value) {
const { value: currentValue } = this.state;
let newValue;
if (value == '=') {
newValue = eval(currentValue).toString();
} else if (value == 'C') {
newValue = 0;
} else if (value == '+/-' && currentValue != '0') {
newValue = `-(${text})`;
} else if (currentValue === '0') {
newValue = value;
} else {
newValue = currentValue.concat(value);
}
this.setState({ value: newValue });
}
render() {
const { value } = this.state;
return (
<div id="body">
<Screen value={value} />
<Buttons onButtonClick={this.handleButtonClick} />
</div>
)
}
}
....
const Button = ({ value, onClick }) => (
<button type="button" onClick={() => onClick(value)}>
{value}
</button>
)
I have renamed your function add and changed its behaviour. Instead of getting/updating the values to DOM, it uses the state in the <Calculator /> component.
You can see the working code here: https://jsfiddle.net/zvxpkq46/
class Button extends React.Component {
constructor(props) {
super(props);
}
add() {
let value = this.value;
let text = document.getElementByID("screenInput").value;
if (value == "=") {
document.getElementById("screenInput").innerHTML = eval(text).toString();
} else if (value == "C") {
document.getElementById("screenInput").innerHTML = "0";
} else if (value == "+/-" && text != "0") {
document.getElementById("screenInput").innerHTML = `-(${text})`;
} else if (value == "d" || (text.slice(-1) != value && text != "0")) {
document.getElementById("screenInput").innerHTML = text.append(value);
}
}
render() {
return (
<button type="button" value={this.props.value} onClick={() => this.add()}>
{this.props.value}
</button>
);
}
}
Constructor doesn't have any initial state set, we don't need it.
Auto bind your function.
add = (button) => {
let value = this.buttonRef.value;
// rest of your code
}
Change the render this way:
render() {
return (
<button ref={(button) => { this.buttonRef = button; }} type="button" value={this.props.value} onClick={() => this.add()}>
{this.props.value}
</button>
);
}
I also suggest avoiding referencing document directly with document.getElementById in react. Instead manage a state variable.
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?
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 dangerouslySetInnerHTML (ideally, so you don't have to create DOM elements via dangerouslySetInnerHTML at all). But answering the question you asked: (You've clarified the use case; solution #1 below applies and isn't poor practice.)
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 the div, but then only take action if the click passed through the b element.
Use a ref on the div, and then hook the click handler up in componentDidMount and componentDidUpdate (finding the b element within the div via querySelector 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.
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>
}
}
}
})
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.
You can make the parent tag a <form> and set the onClick="SomeFunction()".
From the child tag that has the HTML string, set type="button".
let string= "Hello <b type='button'>Click here</b>";
render() {
return (
<form onClick={SomeFunction} dangerouslySetInnerHTML =
{this.createMarkup(value)}/>
}
createMarkup = value => {
return { __html: value };
};
I am trying to create a keypress listener for my React Js calculator app and when I add the event listener in, it detects additional key presses the more I press. Is there a better place to put the event listener? When I press 1234, I get
122333344444444
/****************Button Component*************/
class CalcApp extends React.Component {
state = {
value: null,
displayNumbers: '0',
selectedNumbers: [],
calculating: false,
operator:null
};
selectMath = (selectedMath) =>{
const {displayNumbers, operator,value} = this.state;
const nextValue = parseFloat(displayNumbers)
console.log(selectedMath);
/**do math and other methods*/
render() {
document.addEventListener('keydown', (event) => {
const keyName = event.key;
if(/^\d+$/.test(keyName)){
this.selectButton(keyName)
console.log(keyName);
}
});
return (
<div>
<Display displayNumbers={this.state.displayNumbers}
selectedNumbers={this.state.selectedNumbers}/>
<Button selectedNumbers={this.state.selectedNumbers}
selectButton ={this.selectButton}
selectC = {this.selectC}
displayNumbers={this.state.displayNumbers}
selectDot = {this.selectDot}
selectMath = {this.selectMath}/>
</div>
);
}
}
let domContainer = document.querySelector('#app');
ReactDOM.render(<CalcApp />, domContainer);
Remove document.addEventListener listener from render().
The method is being called whenever the components needs to re-render (changes of state / props) which attaches yet another event listener.
Suggestion: Move document.addEventListener to componentDidMount() - executed only once, and remove it via document.removeEventListener on componentWillUnmount to prevent memory leaks.
Please read code first.
After css processing, it seems like memo application's single memo paper.
The goal of the component is to print a 1 when clicked(in real, the goal is to hadding redux store's state).
When i click outside of div component, it works very well. ( it printed '1' )
but when i clicked inner div component(title, date,content), onClick event also proceed ( it printed '')
how can i prevent non-valued print?
My code :
class container extends Component {
handleState = (event) => {
console.log(event.target.id)
}
render(){
return(
<div onClick={handleState} id={value}>
<div>title</div>
<div>date</div>
<div>content</div>
</div>
)
}
}
container.defaultprops = {
value: 1
}
thanks.
You can use currentTarget:
handleState = (event) => {
console.log(event.currentTarget.id)
}
About difference between target and currentTarget:
https://stackoverflow.com/a/10086501/5709697
You can use currentTarget to check if it's the target since you bound the handler to the parent e.g.
handleState = (event) = > {
if (event.target == event.currentTarget) {
console.log(event.target.id)
}
}