I have a problem which requires me to store the texted of a referenced element in an array.
Now, I first want to display the text for each element(paragraph element with "ebookName" class) in the console, before storing it in the array.
But I have been having problems... Whenever I click an element, the console just logs the previous elements text always. I want for each paragraph element to log that specific elements text, not the previous one
Link to JS code:
import React from 'react'
import "./Styles/Ebook.css"
import { useRef } from 'react';
function Ebook() {
const bookName = useRef();
let ebookData = JSON.parse(sessionStorage.getItem("ebook"));
/*function that displays the specific text of a specific element onto the console*/
const elementLogFunction = () =>{
console.log(bookName.current)
}
return (
<section id="musicRender">
{ebookData.results.map((ebook, i)=>{
return (
<div key={i} className='ebookContentContainer'>
<div className="ebookPicture">
<img src={ebook.artworkUrl100} alt={ebook.trackName} />
</div>
<div className="ebook-description">
<p className="ebookAuthor">Author: {ebook.artistName}</p>
<p ref={bookName} className='ebookAName'>Book Name: {ebook.trackName}</p>
<p className="price">Price: R{(ebook.price * 15.36).toFixed(0)}</p>
<button onClick={elementLogFunction} className="favourites-btn">Add To Favourites</button>
</div>
</div>)
})}
</section>
)
}
export default Ebook
According to your code, ref is only referred to the same data, and the new one will override the old one. In your case, the last book data will be kept.
If you want to have individual book data separately, you can pass a param to elementLogFunction.
You also shouldn't read sessionStorage every rendering. This behavior causes a performance issue due to getting data multiple times. You can use useEffect to read data only once after the first rendering.
function Ebook() {
const [ebookData, setEbookData] = React.useState([]);
//only add data for the first rendering
useEffect(() => {
setEbookData(JSON.parse(sessionStorage.getItem("ebook")));
}, []);
/*function that displays the specific text of a specific element onto the console*/
const elementLogFunction = (ebook) =>{
console.log(ebook.trackName)
}
return (
<section id="musicRender">
{ebookData.results.map((ebook, i)=>{
return (
<div key={i} className='ebookContentContainer'>
<div className="ebookPicture">
<img src={ebook.artworkUrl100} alt={ebook.trackName} />
</div>
<div className="ebook-description">
<p className="ebookAuthor">Author: {ebook.artistName}</p>
<p ref={bookName} className='ebookAName'>Book Name: {ebook.trackName}</p>
<p className="price">Price: R{(ebook.price * 15.36).toFixed(0)}</p>
<button onClick={() => elementLogFunction(ebook)} className="favourites-btn">Add To Favourites</button>
</div>
</div>)
})}
</section>
)
}
export default Ebook
Related
Hello I am working on a blog post creation tool and I need a engine when I write in textarea <hr/> I get a line, or when I write <img/> I get an image but it doesn't render.
The post is written in a textarea and should be displayed in a div.
How to do it?
const PostCreate = () => {
const [postValue, changeValue] = useState('')
const handleChangeValue = (e:any) => {
changeValue(e.target.value)
console.log(postValue);
}
return (
<div className='postCreate'>
Create New Post
<textarea onChange={handleChangeValue} value={postValue}/>
<div>
{postValue}
</div>
</div>
)
}
If I write <hr/> I get the string <hr/> instead of a line:
You can use dangerouslySetInnerHTML prop for the div.
<div dangerouslySetInnerHTML={{ __html: postValue }} />
You can check it on this document.
I'm new to react and not sure I'm going about this the right way. What is happening is I'm grabbing data from pokemon api turning that data into cards that show up based on game selected. Data gets transferred via prop pokedex. Clicking on a card gets me the information for later storing/use.
Currently I can click the generated cards(from panelComp) and have only 1 show up. It does change based on what I click but does not replace the blank card. I know I will need a statement that stores the card in each div and wont let you go over 6.
What end goal and looking to do is to have 6 blank cards/panels up top as "empty"(grey boxes). I want to fill these with the selected pokemon cards/panels from the ones generated from PanelComp.
Later will be using the selected cards to make a filter based on pokemon types. I know I will also need to add a click event to those cards so I can remove them back to blank. I have tried a few things to no avail any direction would be greatly appreciated as I just cant grasp this for some reason.
Code has placeholders via div emptyBox just so I can lay it out.
import Panel from './Panel';
import './PanelList.css';
const PanelList = ({ pokedex }) => {
const [card, setCard] = useState(null);
const [panelPick, setPanelPick] =useState(null);
const [isSelected, setIsSelected]= useState(false);
useEffect(() => {
setPanelPick(panelComp[card]);
setIsSelected(true);
},[card]);
const panelComp = pokedex.map((pokemon, i) => {
return <Panel
onChange={num => setCard(num)}
panelId={i}
id={pokedex[i].id}
name={pokedex[i].name}
types={pokedex[i].types}
/>
})
const isItSelected = isSelected;
return (
<div>
<div>
<div>
{isItSelected ? (
<div id='Block1'>{panelPick}</div>
) : (
<div className='emptyBox' id='Block1'></div>
)}
</div>
<div className='emptyBox' id='Block2'>{panelPick}</div>
<div className='emptyBox' id='Block3'></div>
<div className='emptyBox' id='Block4'></div>
<div className='emptyBox' id='Block5'></div>
<div className='emptyBox' id='Block6'></div>
</div>
<div>
{panelComp}
</div>
</div>
);
}
export default PanelList;
import React from 'react';
import './Panel.css'
const Panel = ({id, name, types, panelId, onChange, onChildEvent }) => {
const handleEvent = event => {
onChange(panelId)
};
return (
<div className="PNL" onClick={handleEvent}>
<img className='pokeImg' alt='pokemon img' src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}/>
<div className='Potext'>
<h2>{name[0].toUpperCase() + name.substring(1)}</h2>
<p>Id: {id}</p>
<p>Type: {
types.map(type =>
type.type.name[0].toUpperCase() + type.type.name.substring(1))
.join(', ')
}</p>
</div>
</div>
);
}
export default Panel;
This is my current code for the component and child in question. I can add more if needed from the parent. Not sure if I'm making this too difficult or not. I have been working at this for a few days and just don't know where to go with it. Really trying to learn as I go which I'm not sure if it helps.
Thank you
I am building a recipe search app but I am not sure how to render the big chunk of html representing the 30 recipes I want to have displayed. These recipe card obviously have to change based on what kind of meal I search for. How do I Implement the html into my Js so I can make it change based on the meal type?
I tried to do it with insertAdjecentHTML but since it doesn't replaces the old page with the recipe on but rather ads on to it. It didn't worked:
Nevertheless here is the code:
// recipe card markup
const renderRecipe = () => {
const markup = `
<div class="recipe-results__card">
<div class="recipe-results--img-container">
<img
src="${data.data.hits.recipe.image}"
alt="${data.data.hits.recipe.label}"
class="recipe-results__img"
/>
</div>
<p class="recipe-results__categories">
${limitCategories}
</p>
<h2 class="recipe-results__name">
${data.data.hits.recipe.label}
</h2>
</div>
`;
DOM.recipeGrid.insertAdjacentHTML("beforeend", markup);
};
// fn to display the markup for every search res
export const renderResults = recipes => {
recipes.forEach(renderRecipe);
};
concatenate (push to an array) the HTML so you have ONE update
You can use innerHTML instead of insertAdjacent to replace the content
let html = [];
data.data.hits.forEach(recipe => {
html.push(`
<div class="recipe-results__card">
<div class="recipe-results--img-container">
<img
src="${recipe.image}"
alt="${recipe.label}"
class="recipe-results__img"
/>
</div>
<p class="recipe-results__categories">
${limitCategories}
</p>
<h2 class="recipe-results__name">
${recipe.label}
</h2>
</div>
`) });
document.getElementById("recipeGrid").innerHTML = html.join("");
I am building an application where users can add and delete items of different "types". For example there may be a text type where users can edit the text, and an image type where users can drop in an image.
Adding an item to the list will cause the list to update and display all old items along with the new item. Similarly, deleting an item will remove that item and cause the list to only display the remaining items.
The add functionality works perfectly fine. The issue comes with deletes when the items are of different types. Here is my (simplified) code:
// List.svelte
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
type: "text",
value: "Test 1"
},
{
type: "img",
value: "https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
// Wrapper.svelte
<script>
import {onMount} from "svelte";
export let item;
export let store;
export let myIndex;
let DynamicItem;
const handleDeleteItem = () => {
store.update((items) => ([...items.slice(0, myIndex), ...items.slice(myIndex + 1)]))
}
onMount(async () => {
let imported;
if(item.type === "text") {
imported = await import("./TextItem.svelte")
} else {
imported = await import("./ImgItem.svelte")
}
DynamicItem = imported.default;
})
</script>
<svelte:component this={DynamicItem} data={item} />
<button on:click={handleDeleteItem}>Delete</button>
// TextItem.svelte
<script>
export let data;
</script>
<div contenteditable="true" bind:textContent={data.value} />
// ImgItem.svelte
<script>
export let data;
</script>
<img src="{data.value}" width="100px" height="100px" alt="img alt" />
When the user deletes the TextItem from the list by clicking on the first delete button, the ImgItem no longer displays the image and instead only renders the src attribute as text.
When looking at the HTML output, before the delete action it looks like this:
<body>
<div contenteditable="true">Test 1</div>
<button>Delete</button>
<img src="https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
And afterwards, it looks like this:
<body>
<div contenteditable="true">https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif</div>
<button>Delete</button>
</body>
So it looks like Svelte is not truly deleting the TextItem but moving the content from the next item into its place.
How can I fix my code so that items are truly deleted? The goal should be to produce the following HTML by performing the same action:
<body>
<img src="https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
Any help is appreciated. Thank you
So it looks like Svelte is not truly deleting the TextItem but moving the content from the next item into its place.
Yes, absolutely, this is what is happening.
Svelte (like React) only recreate an element in a given position when its nature has obviously changed, for example if the tag name is not the same. Otherwise, it is considered that it is the same element / component with just some props / state that has changed.
In order to hint the framework that the identity of an element has changed, you must the key functionality. In React, it's a literal key prop. In Svelte, keys are only supported in {#each ...} blocks (for now?). The syntax is the following (docs):
{#each expression as name, index (key)}...{/each}
# also works, I think it is an oversight in the docs currently
{#each expression as name (key)}...{/each}
The value of the key must be unique to each item for it to work. Typically you'd use an id of sort. But truly, it can be anything. If the value change for the element / component at a given position, then it will be recreated.
So, in your case, you could fix your issue by using the item type as a key, for example:
{#each $listStore as item, i (item.type)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
This fixes the issue you're asking about, but it's not such a great idea I must say. For other reasons, you'd better add real unique ids to your items and use them instead. With the above code, items of the same type will continue to share DOM elements, and that's probably not what you want. You may have issues with focus or internal state (value) of DOM elements like inputs, for example.
So, something like this is better and is probably the way to go:
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
id: 1,
type: "text",
value: "Test 1"
},
{
id: 2,
type: "img",
value: "https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i (item.id)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
When a state is changed, React triggers componentDidUpdate() method, and by then I do:
componentDidUpdate: function () {
React.render(new SubmitButton, $('.uploader-submit').get(0));
}
As you saw, I'm rendering a SubmitButton when a specific state is changed, but my question is: is this the best behavior to get this feature done?
My scenario is: I'm uploading a photo. When the input[type=file] is changed, I create a new state property and then the componentDidUpdate() is triggered, invoking the SubmitButton.
This is my render() method:
render: function () {
return (
<div className="uploader">
<header className="uploader-header">
<div className="uploader-actions pull-left">
<div className="uploader-submit"></div>
<CancelButton router={this.props.router} />
</div>
<UploadButton callback={this.imageSelectedCallback} />
</header>
<Preview imageUri={this.state.imageUri} />
</div>
)
}
Couldn't I do something like the <Preview /> component? I mean, it is there, but something just appears when this.state.imageUri is different of null. This is the implementation of Preview:
var Preview = {
render: function () {
return (
<img src={this.props.imageUri} />
)
}
};
module.exports = React.createClass(Preview);
Yes, I know — "Preview" is invisible by default because it is an image, but I want to know if there's another approach to reach what I want: to show something based on a state, using the render method.
React doesn't render falsy values, be it a component or an attribute (like in the Preview case), e.g.
<div>{null}</div>
<img src={null} />
renders to
<div></div>
<img/>
So typically you just create a variable and conditionally assign it a component or null as was also suggested in another answer:
var button = null;
if(myConditionForShowingButton) {
button = <SubmitButton />;
}
-- or simply --
var button = myConditionForShowingButton ?
<SubmitButton /> :
null;
In cases where the component gets bigger it's typically more readable and cleaner to have a subroutine for rendering that part
var complexComponent = condition ?
this.renderComplexComponent() :
null
Yes. If-Else in JSX.
render: function () {
var submitButton;
if (this.state.imageSelected)
submitButton = <SubmitButton />;
return (
<div className="uploader">
<header className="uploader-header">
<div className="uploader-actions pull-left">
<div className="uploader-submit">{ submitButton }</div>
<CancelButton router={this.props.router} />
</div>
<UploadButton callback={this.imageSelectedCallback} />
</header>
<Preview imageUri={this.state.imageUri} />
</div>
)
}