How to retrieve information on a specific button when clicked? - reactjs

I'm making a film review site where users can search for the title of a film and when entered, a list of films matching the entered title will appear.
I made it so that each film and its corresponding information is contained within a button. I want to add an event listener to the film buttons that will listen to when the button is clicked and retrieve an image link of the movie poster from that specific button that was clicked and plug it into a text field with the ID of "imageSrc". The problem is that currently, it only retrieves the image link of the last film button that is displayed on the page rather than retrieving a specific film button I clicked and the last image link is plugged into the text field "imageSrc" right after entering the search (no film buttons were clicked, it just automatically plugs in the image link associated with the last film result displayed).
function FilmCard({ film }) {
function displaySearchResults({ film }) {
let results = [];
for (let i = 1; i < 5; i++) {
if (film.results[`${i}`]) {
results +=
`<button class='singleFilmCards' onclick=${(document.getElementById(
"imageSrc"
).value = film.results[`${i}`].image)}>` +
"<div class='film-wrapper'>" +
`<img src="${film.results[`${i}`].image}">` +
"<h2>" +
film.results[`${i}`].title +
"</h2>" +
"<h3>" +
film.results[`${i}`].description +
"</h3>" +
"<div>" +
"</button>";
}
}
return results;
}
if (!film) return <></>;
return (
<>
<div
className="FilmList"
dangerouslySetInnerHTML={{
__html: displaySearchResults({ film }),
}}
>
{/* <img alt="Movie Posters" src={film.results[0].image} />
<h2>{film.results[0].title}</h2>
<p>{film.results[0].description}</p> */}
</div>
</>
);
}

This happens because the index variable i in your loop will always have value of 5 in all the onclicks. There is very good reason why setting a pure html in react is done with the dangerouslySetInnerHTML prop - to let the developer know that he/she's doing something that shouldn't be done (there are cases when it's necessary, extremely rare though...)
What you should do, is to make the button into it's own component and handle the events via react event callbacks.
You could try something like this:
function FilmButton({ result, onImageChange }) {
return (
<button
className="singleFilmCards"
onClick={() => onImageChange(result.image)}
>
<div className="film-wrapper">
<img src={result.image} />
<h2>{result.title}</h2>
<h3>{result.description}</h3>
</div>
</button>
);
}
function FilmCard({ film }) {
const handleImageChange = (imageUrl) => {
document.getElementById("imageSrc").value = imageUrl;
};
if (!film) {
return <></>;
}
return (
<div className="FilmList">
{film.results.map((result, index) => (
<FilmButton
key={index}
result={result}
onImageChange={handleImageChange}
/>
))}
</div>
);
}
...in fact, you definitely might want to consider also rewriting the document.getElementById("imageSrc").value = imageUrl in such way, that you won't be manipulating the DOM directly. Because this completely bypasses React.js. There are many ways you rewrite this to use React - from passing the new imageUrl via prop callback into higher component (same as the onImageChange in my example), to using a React context (context would probably be an overkill though :)

Related

React function in displaying returned value not working

I am trying to display calculated values from a function called renderAnalysis - all which works until the bottom of the function where I am trying to return a div with the values that are found. However, this isn't displaying (all values are returned though in the console when I test it out) and was wondering what I need to adjust to prevent the values I can show in the console? I think I'm doing a render call wrong or something... I've added the code for reference. Thank you!
renderAnalysis = () => {
let reactions = this.state.reactions;
let foods = Object.keys(reactions);
foods.map((food) => {
let reactionDescriptions = reactions[food];
let reactionDescriptionKeys = Object.keys(reactionDescriptions);
let responseKey = Object.keys(reactionDescriptions);
responseKey.map((singleDescription) => {
let value = reactionDescriptions[singleDescription];
// RETURN VALUE NOT SHOWING
return <div> {(food, singleDescription, value)} </div>;
});
});
};
Further information: There is a toggle I made in state that if someone clicks a button it triggers this function. Basically
<button onClick={this.displayRenderAnalysisToggle}>
Render Analysis
</button>{" "}
<br />
{this.state.displayRenderAnalysisToggle ? this.renderAnalysis() : null}
</div>

How can I create a parent html element by appending sub element from an object?

In my react app I need to return a line which will be created based on a list.
Here is the object,
searchCriteria: {
op_company: "039",
doc_type: "ALL"
}
and in my UI, i need to show it as a paragraph with bold values. So the hard coded code would be like below
<p>Download request for op_company: <b>{searchCriteria.op_company}</b>, doc_type: <b>{searchCriteria.doc_type}</b></p>
But the object(searchCriteria) will be changed based on the user request. So I tried like below.
const getSearchCriteria = (criteria) => {
let searchCriteria = []
searchCriteria.push('Download request for')
Object.keys(criteria).forEach((key) => {
if(criteria[key] !== '') {
searchCriteria.push(` ${key}: ${criteria[key]},`)
}
});
return searchCriteria;
}
return (
<p>
{getSearchCriteria(searchCriteria).map((item) => <span key = {item}>{item}</span>)}
</p>
);
here i'm getting the expected output. But I can't get the value as bold (highlighted). Is there another way to directly deal with html elements?

Have got id of clicked button in React but then want to use that id within an if

I have a Next and Prev button below a list on my spfx based webpart.
I also have another list below it with another two Next and Prev buttons.
I want to distinguish between the two sets of buttons and use the two functions that I'm using already:
private _listANext(e) {
let currentPage = this.state.ListAPage;
let highestPageNo = this.state.ListAPages;
currentPage = Math.min(highestPageNo,currentPage + 1 );
this.setState({
ListAPage: currentPage,
});
console.log(e.target.id);
}
private _listAPrev(e) {
let currentPage = this.state.ListAPage;
currentPage = currentPage <= 1 ? 1 : currentPage - 1;
this.setState({
ListAPage: currentPage,
});
console.log(e.target.id);
}
and the buttons in the render:
<DefaultButton
id="aprev"
text="Prev"
onClick={(e) => this._listAPrev(e)}
/>
<span> {' '}</span>
<DefaultButton
id="anext"
text="Next"
onClick={(e) => this._listANext(e)}
/>
The two functions give me the id of the buttons but if I were to use the id's within an if, would the id's stay the same on different browsers? I think there is a better way to do what I'm trying to do!
<DefaultButton
id="aprev"
text="Prev"
onClick={(e) => this._listAPrev(e,id)}
/>
you can pass the id by this.and get the id and use it.
private _listAPrev(e,id) {}

ReactJS - embedding custom element in a custom element

I just started to learn ReactJS and encountered a problem that I can't solve.
I'm creating a basic application for TV Shows. I have a bootstrap tab for every season of a show and within this tab I want to list all the episodes of the selected season. The problem is that I have to create the tabs in a loop, and within this loop I should have another loop for the episodes.
I'm trying to do something like this:
EpisodeCards(props) {
return (
<div>
This should contain the details of the episodes
</div>
)
}
SeasonTabs(props) {
console.log('Seasons: ', props.seasons)
let tabs = [];
let episodes = [];
let currentSeason = 1;
let id;
let aria;
for(let season of props.seasons) {
episodes = [];
id="nav-season" + currentSeason;
aria = "nav-season" + currentSeason + "-tab";
tabs.push(<div className="tab-pane fade" id={id} role="tabpanel" aria-labelledby={aria}><this.EpisodeCards episodes={season}></this.EpisodeCards></div>);
currentSeason++;
}
return (
<div className="tab-content py-3 px-3 px-sm-0" id="nav-tabContent">
{tabs}
</div>
)
}
For this I am getting the following error:
Unhandled Rejection (TypeError): Cannot read property 'EpisodeCards' of undefined
How can this be done in the 'react way'? Thanks in advance.
Change
SeasonTabs(props)
to
SeasonTabs = (props) =>
You want to access a class property using this but by default its not binded to the function (ES5 only) by creating the functions using arrow () =>(new ES6 syntax) it automatically bind this to the function.
For Example:
class Test extends Component{
constructor(props){
this.testFn= this.testFn.bind(this);
}
testFn(){
console.log(this); //will output
}
testFn2(){
console.log(this); // undefined
}
testFn3 = () =>{
console.log(this); //will output
}
}
Reactjs is all about components, everything is component. If a function return a react element or Custom element then is a component.
You are creating a functional component inside of a class component, while this approach may work but this is not appropriate way to make component.
It is better to use composition, your code will be more clear, easier
to read, better maintainability and you can use component in other
components.
My solution:
function EpisodeCards(props) {
return (
<div>
This should contain the details of the episodes
</div>
)
}
SeasonTabs(props) {
console.log('Seasons: ', props.seasons)
let tabs = [];
let episodes = [];
let currentSeason = 1;
let id;
let aria;
for(let season of props.seasons) {
episodes = [];
id="nav-season" + currentSeason;
aria = "nav-season" + currentSeason + "-tab";
//There is no need for this keyword
tabs.push(<div className="tab-pane fade" id={id} role="tabpanel" aria-labelledby={aria}><EpisodeCards episodes={season}/></div>);
currentSeason++;
}
return (
<div className="tab-content py-3 px-3 px-sm-0" id="nav-tabContent">
{tabs}
</div>
)
}

React contenteditable cursor jumps to beginning

I am using the module react-simple-contenteditable to enable editing of a fill in the blank worksheet. The reason I must use a content editable element instead of an input element is because I want the text of the problem to wrap. For example, if a problem has one blank, it divides the text into three sections the part before the blank, the blank, and the part after. If I were to represent the outer two as separate divs (or input fields), then the text would not wrap like a paragraph. Instead, I must have a single contenteditable div that contains an input field for the blank and free text on either side.
The text is wrapping like I want it, but when I type text in the contenteditable field, the cursor jumps to the beginning. I don't understand why because I tried the example on the module's github site and it works perfectly, and although my implementation is a bit more complicated, it works essentially the same.
Here is my render function that uses <ContentEditable /> :
render() {
const textPieces =
<div className='new-form-text-pieces'>
{
this.props.problem.textPieces.map( (textPiece, idx) => {
if (textPiece.blank) {
return (
<div data-blank={true} className='blank' key={ textPiece.id } style={{display: 'inline'}}>
<input
placeholder="Answer blank"
className='new-form-answer-input'
value={ this.props.problem.textPieces[idx].text }
onChange={ (event) => this.props.handleTextPiecesInput(this.props.problemIdx, idx, event.target.value) }
/>
<button className='modify-blank remove-blank' onClick={ (event) => this.props.removeBlank(this.props.problemIdx, idx) }>-</button>
</div>
);
} else {
let text = this.props.problem.textPieces[idx].text;
const placeholder = idx === 0 ? 'Problem text' : '...continue text';
// text = text === '' ? placeholder : text;
if (text === '') {
text = <span style={{color:'gray'}}>{placeholder}</span>;
} else {
}
return (
this.props.isTextSplit ?
<TextPiece
key={ textPiece.id }
problemIdx={this.props.problemIdx}
textPieceIdx={idx}
dropBlank={this.props.dropBlank}
moveBlank={this.props.moveBlank}
>
<div style={{display: 'inline-block', }}>{text}</div>
</TextPiece>
: text
);
}
})
}
</div>;
return (
this.props.isTextSplit ? textPieces :
<ContentEditable
html={ReactDOMServer.renderToStaticMarkup(textPieces)}
className="my-class"
tagName="div"
onChange={ (event, value) => this.props.handleProblemChange(event, this.props.problemIdx, value) }
contentEditable='plaintext-only'
/>
);
}
Here is the onChange function:
handleProblemChange(event, problemIdx) {
const problems = cloneDeep(this.state.problems);
event.target.children[0].childNodes.forEach( (textPieceNode, idx) => {
if (textPieceNode.constructor === Text) {
problems[problemIdx].textPieces[idx].text = textPieceNode.wholeText;
} else {
problems[problemIdx].textPieces[idx].text = textPieceNode.childNodes[0].value;
}
});
this.setState({ problems });
}
And here is the state it refers to, just to make thing clear:
this.state = {
problems: [
{
id: shortid.generate(),
textPieces: [
{
text : "Three days was simply not a(n)",
blank : false,
id: shortid.generate(),
},
{
text : "acceptable",
blank : true,
id: shortid.generate(),
},
{
text : "amount of time to complete such a lot of work.",
blank : false,
id: shortid.generate(),
}
]
}
Thanks so much
Long story short, there is no easy way to do this. I have tried this myself and spent days trying. Basically you have to save the cursor position and reposition it yourself after the update. All of this can be achieved with window.getSelection()
https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection
But it can get really tricky depending on how much your content has changed.
I ended up using draftJS instead. Which is an abstraction over contenteditable div by facebook themselves.
https://draftjs.org/docs/overview.html#content
A bit longer to pick up but you will be able to do a lot more
I had a similar problem using VueJS.
Here is the component containing the contenteditable div :
<Text #update-content="updateContent" :current-item-content="item.html_content"/>
Here is the prop definition in Text.vue component :
const props = defineProps({
currentItemContent: {
type: String,
default: ''
}
})
Here is the contenteditable div in Text.vue component :
<div
id="text-editor"
ref="text_editor"
class="mt-3 h-full w-full break-words"
contenteditable="true"
#input="updateContent"
v-html="currentItemContent"
>
Here is the method triggered on #update-content event
const item = computed(() => { ... })
(...)
function updateContent(content) {
item.value.html_content = content
}
The problem here is injecting the item.html_content value as a props triggers a re-render of the contenteditable div.
Because it's mutating it's value in the updateContent method and as the computed (item) is beeing updated, so does the prop value, v-html detects the updated value and triggers a re-render.
To avoid this, i removed the v-html binding :
<div
id="text-editor"
ref="text_editor"
class="mt-3 h-full w-full break-words"
contenteditable="true"
#input="updateContent"
>
And initialized the value of the contenteditable div in a onMounted hook :
const text_editor = ref(null)
(...)
onMounted(() => {
if (props.currentItemContent !== '') {
text_editor.value.innerHTML = props.currentItemContent
}
})
I don't know if there is a better solution for this but it's working fine for me. Hope this helps someone

Resources