Svelte.js - Can't Properly Delete Item From List - javascript

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}

Related

How to track changes of a referenced element with React?

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

React.js - Done a API fetch that renders all items, now I want to be able to route on each item to their "own" page with JSON info

I have created an React site that renders all items inside an API Fetch on a page. What I want now is to be able to press each item that renders and be able to get routed to a new component that shows "more" info about that item.
Below I have the following code that takes in the "input" that you can use to search for all items or for a specific item.
const AgentSearch = (e) => {
e.preventDefault();
function capitalizeName(input) {
return input.replace(/\b(\w)/g, (s) => s.toUpperCase());
}
console.log('you hit search', input);
dispatch({
type: actionTypes.SET_SEARCH_TERM,
term: capitalizeName(input),
});
//do something with input
history.push('/findagent');
};
return (
<form className='search'>
<div class='search__input'>
<SearchIcon className='search__inputIcon' />
<input value={input} onChange={(e) => setInput(e.target.value)} />
</div>
Here is the code that renders all items:
eachName = data.map((item) => {
return (
<tr>
<td class='text-left'>{item.name}</td>
<td class='text-left'>{item.agency}</td>
</tr>
);
});
Basically what I would like to do is to "catch" the {item.name} and put that into a new query into the fetch once you press that item on the list that got created.
I tried to create a button that covers the whole class and then put {item.name} as input, but that does not work, also tried to make a "fake" input window that has the {item-name} stored for each item on list, even though the {item-name} gets onto the input window, once i push the button that its connected to, it says it doesn't have any value.
Does anyone know of any clean idea for this? I'm new to React so this might be really easy, haha.
Thanks!
The simplest way is to do onCLick on the td. See the code below.
const eachName = data.map((item) => {
return (
<tr>
<td class='text-left' onClick={() => handleNameClick(item.name)}>{item.name}</td>
<td class='text-left'>{item.agency}</td>
</tr>
);
});
You can define the handleNameClick function. You'll get the item.name as the parameter. See below.
const handleNameClick = itemName => {
//your code to send request for the clicked name
}

Replace blank div card/panel with another one that is clicked in React

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

How to deal with a value that might be an array, or might be only a single item react component

I am very new to react and am trying to make a user survey, in this Component I am working with checkboxes which I find a little tricky. I want the user to choose 2 options and then put a comma between the two chosen options, how can I do that?
Earlier I used stageArtCategory.join(', ') in my Summary.js component but that broke the rendering of Summary in case the user chose only one instead of two options...
CODE:
import React from 'react';
import NextQuestionButton from './NextQuestionButton'
import Popup from './Popup'
const QuestionStageArtCheckbox = ({
stageArtCategory,
setStageArtCategory,
page,
setPage,
onNextQuestion
}) => {
const onStageArtChange = (stageArtValue) => {
if (stageArtCategory.includes(stageArtValue)) {
setStageArtCategory(stageArtCategory.filter((item) => item !== stageArtValue))
} else {
setStageArtCategory([...stageArtCategory, stageArtValue])
}
}
const stageartGroup = [
"I'm for experimental shit, I need to see something I can not immediately understand",
"I want to dance with my kid",
"Opera",
"Theatre",
"Musical",
"Concert",
"I just miss the feeling of collectivity, and long to be able to see something with another person's eye - just once, please!",
"I want to drink beer with my friends after a show"
]
return (
<article className="form-question-3">
{/* Q */}
<p className="form-question" tabIndex="0">
What kind of stage art would you like too experience post Covid-19? Pick 2 as they are made to overlap a little! <span role="img" aria-label="smiling emoji with one eye blinking">😊</span>
</p>
{/* A */}
<div className="question-content-container-3">
{stageartGroup.map((stagearts) => (
<span className="form-checkbox-question-container" key={stagearts}>
<label className="checkbox-label" htmlFor={stagearts}>{stagearts}</label>
<input
id={stagearts}
type="checkbox"
className="form-checkbox"
checked={stageArtCategory.includes(stagearts)}
onChange={() => onStageArtChange(stagearts)}
/>
</span>
))}
<div className="buttons-container-3">
<NextQuestionButton
page={page}
setPage={setPage}
currentState={stageArtCategory.length}
defaultState={0}
message="Please choose what kind of overlapping stage arts you would like to experience first!"
onClick={onNextQuestion}
button="Next question button"
buttontext="Next question"
/>
</div>
</div>
</article>
)
}
export default QuestionStageArtCheckbox
I prefer to always treat such values as an arrays. So, whenever I need to use such value, I turn it into array with .concat method.
function arbitraryFunction(arrayOrSingle) {
let array = [].concat(arrayOrSingle);
// do something with array
}
Initialised the useState as an empty string earlier but changed it to be an empty array. This solved the problem.
const [stageArtCategory, setStageArtCategory] = useState([])

Proper way of object copy from one element to another in Vue.js

I am new to Vue.js (I mostly use PHP) and I am trying to creating simple view where user can add an object from one component and place it's copy into another component.
Main template
<template>
<div class="left">
<TaskList :tasks="tasks" v-on:pinned-add-task="pinnedAddTask" />
</div>
<div class="right">
<PinnedList :pinned="pinned" />
</div>
</template>
TaskList
<template>
<div class="task-list">
<div v-for="task in tasks" :key="task.id">
<TaskItem :task="task" v-on:pinned-add-task="$emit('pinned-add-task', task)" />
</div>
</div>
</template>
TaskItem
<template>
<div>
<p>{{task.name}}</p>
<button v-on:click="$emit('pinned-add-task', task)">+</button>
</div>
</template>
And as far as I am aware object "task" is passed by reference and when I try to create an empty object or an array and insert "task" into that newly created object/array when I change original "task" it is also being changed inside that new object and I don't want that.
I am getting my data (tasks) from API that I have created and I am using pagination system so I want to be able to switch pages without losing it from the pinned page.
I created code which looks like this but I don't like it and I don't think that's a good way to do this:
pinnedAddTask(item) {
let pQuantity = 1; // I use this value because I want to be able to pin one task multipletimes
let left = this.pinned;
let right = [];
for (let task of this.pinned) {
if(item.id == task.id) {
pQuantity = task.quantity + 1;
left = this.pinned.filter(eItem => eItem.id < item.id);
right = this.pinned.filter(eItem => eItem.id > item.id);
}
}
const clone = {...item, quantity: pQuantity};
this.pinned = [...left, clone, ...right];
}
Can anyone confirm or reject this?
Yes this one is fine if thats just a shallow copy [ level-one object].
But if you are having a nested object then you might have to use recursive methodology or use any external libary like lodash

Categories