Prevent Svelte transition on first render after onMount - javascript

I'm working with the following code in Svelte 3.55.0.
First, I have an empty webpage because I have no items to render. Then, onMount fires and I fetch some items from an API. Finally, the items get rendered.
<script>
import { slide } from "svelte/transition";
import { onMount } from "svelte";
// Initially we have no items.
let items = [];
let id = 0;
onMount(() => {
// Fetch items from API.
items = [
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
];
});
function addItem() {
items = [
...items,
{id: id, name: `item ${id++}`},
];
}
</script>
<div>
<button on:click={addItem}>add</button>
{#each items as it (it.id)}
<div transition:slide>
<p>{it.id} {it.name}</p>
</div>
{/each}
</div>
The problem is that fetch gets like 50 items and I don't want to play transitions for any of them. However, I do want transitions when individual items are added or removed only inside of the list.
Is there a way to achieve this effect?
Sandbox: https://codesandbox.io/s/blue-shape-njxx9o?file=/App.svelte

This could be achieved by adding an #if block around the main element so it's only rendered after the items are fetched and adding the |local flag to the transition - tutorial
REPL
(Like here the transition only seem to play correctly if the outer element has display:flex)
<script>
import { slide } from "svelte/transition";
import {onMount} from 'svelte'
let items = [];
let id = 0;
let itemsFetched = false
onMount(async() => {
setTimeout(() => {
items = [
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
];
console.log('fetched')
itemsFetched = true
},1000)
})
function addItem() {
items = [
...items,
{id: id, name: `item ${id++}`},
];
}
</script>
<button on:click={addItem}>add</button>
{#if itemsFetched}
<div id="outer">
{#each items as it (it.id)}
<div transition:slide|local={{duration: 2000}}>
<p>{it.id} {it.name}</p>
</div>
{/each}
</div>
{/if}
<style>
#outer {
display: flex;
flex-direction: column;
}
</style>
Alternative with #await REPL
<script>
import { slide } from "svelte/transition";
import {onMount} from 'svelte'
let items = [];
let id = 0;
function fetchItems() {
return new Promise(res => {
setTimeout(() => {
items = [
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
];
console.log('fetched')
res()
},1000)
})
}
function addItem() {
items = [
...items,
{id: id, name: `item ${id++}`},
];
}
</script>
<button on:click={addItem}>add</button>
{#await fetchItems() then _}
<div id="outer">
{#each items as it (it.id)}
<div transition:slide|local={{duration: 2000}}>
<p>{it.id} {it.name}</p>
</div>
{/each}
</div>
{/await}
<style>
#outer {
display: flex;
flex-direction: column;
}
</style>

Related

How to build a react button that stores the selection in an array

I am trying to create a list of buttons with values that are stored in a state and user is only allowed to use 1 item (I dont want to use radio input because I want to have more control over styling it).
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState([]);
const clickTask = (item) => {
setTask([...task, item.id]);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
const chosenTask = (item) => {
if (task.find((v) => v.id === item.id)) {
return true;
}
return false;
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={chosenTask(item) ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-cvhivt?file=/src/App.js
I am trying to only allow 1 item to be added to the state at all the time, but I dont know how to do this?
Example output is to have Easy and Fast in task state and is selected. If user click on Easy and Cheap, select that one and store in task state and remove Easy and Fast. Only 1 item can be in the task state.
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState();
const clickTask = (item) => {
setTask(item);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={item.id === task?.id ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
Is this what you wanted to do?
Think of your array as a configuration structure. If you add in active props initialised to false, and then pass that into the component you can initialise state with it.
For each task (button) you pass down the id, and active state, along with the text and the handler, and then let the handler in the parent extract the id from the clicked button, and update your state: as you map over the previous state set each task's active prop to true/false depending on whether its id matches the clicked button's id.
For each button you can style it based on whether the active prop is true or false.
If you then need to find the active task use find to locate it in the state tasks array.
const { useState } = React;
function Tasks({ config }) {
const [ tasks, setTasks ] = useState(config);
function handleClick(e) {
const { id } = e.target.dataset;
setTasks(prev => {
// task.id === +id will return either true or false
return prev.map(task => {
return { ...task, active: task.id === +id };
});
});
}
// Find the active task, and return its text
function findSelectedItem() {
const found = tasks.find(task => task.active)
if (found) return found.text;
return 'No active task';
}
return (
<section>
{tasks.map(task => {
return (
<Task
key={task.id}
taskid={task.id}
active={task.active}
text={task.text}
handleClick={handleClick}
/>
);
})};
<p>Selected task is: {findSelectedItem()}</p>
</section>
);
}
function Task(props) {
const {
text,
taskid,
active,
handleClick
} = props;
// Create a style string using a joined array
// to be used by the button
const buttonStyle = [
'taskButton',
active && 'active'
].join(' ');
return (
<button
data-id={taskid}
className={buttonStyle}
type="button"
onClick={handleClick}
>{text}
</button>
);
}
const taskConfig = [
{ id: 1, text: 'Easy and Fast', active: false },
{ id: 2, text: 'Easy and Cheap', active: false },
{ id: 3, text: 'Cheap and Fast', active: false }
];
ReactDOM.render(
<Tasks config={taskConfig} />,
document.getElementById('react')
);
.taskButton { background-color: palegreen; padding: 0.25em 0.4em; }
.taskButton:not(:first-child) { margin-left: 0.25em; }
.taskButton:hover { background-color: lightgreen; cursor: pointer; }
.taskButton.active { background-color: skyblue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Svelte iterate over array of objects

Having this fixed array of objects like this:
export let items = [
{
name: 'package',
subname: 'test'
},
{
name: 'package',
subname: 'test1'
},
{
name: 'pack',
subname: 'test2'
}
]
it is possible to use iterate using each or anything else to get something like this?
<div class='item'>
<div class='name'>package</span>
<div class='subname'>test</span>
<div class='subname'>test1</span>
</div>
<div class='item'>
<div class='name'>pack</span>
<div class='subname'>test2</span>
</div>
A solution would be to merge the items together in an object that has for keys the different names and as value an array of all subnames just like that:
{
"package": [
"test",
"test1"
],
"pack": [
"test2"
]
}
You can do that in a getSubnamesByName function:
const getSubnamesByName = (items) => {
const mergedItems = {}
items.forEach(({name, subname}) => {
if (mergedItems[name]) mergedItems[name].push(subname)
else mergedItems[name] = [subname]
})
return mergedItems
}
Then just store the result in a mergedItems variable that is populated when the component is mounted:
<script>
import { onMount } from 'svelte';
const ITEMS = [ /* ... */ ];
let mergedItems = {}
const getSubnamesByName = (items) => { /* ... */}
onMount(async () => {
mergedItems = getSubnamesByName(ITEMS)
})
</script>
And finally iterate on this object keys and values by using 2 #each blocks:
{#each Object.keys(mergedItems) as name}
<div class='item'>
<div class='name'>{name}</div>
{#each mergedItems[name] as subname}
<div class='subname'>{subname}</div>
{/each}
</div>
{/each}
Have a look at the REPL.

How can you check if there is an existing value in the array and not add it another time if so?

I'm trying to push items to an array but I want that if there's a similar item in an array, to just update the count to 1 and not add the item another time on the array. I've tried plenty of things such as the include() function but it is not working as expected, because when I put item.includes(product) as I'm mapping through each product, whenever I add an item, the count gets updated for each product...
import React, {useState, useContext} from 'react'
import data from './data.js'
import useCountsContext from './context/useCountsContext.js'
var uniqid = require('uniqid');
function Shop({ data }) {
const {count, setCount} = useContext(useCountsContext)
const {item, setItem} = useContext(useCountsContext)
const addCart = (productsId) => {
setCount(count + 1)
data.forEach((product) => {
if (item.includes(product)) {
product.count += 1
} else if (product.id === productsId) {
setItem(item.concat(product))
}
})
}
console.log(item)
return (
<div>
<h1>Shop</h1>
<div className="div___shop">
{data.map(({id, img, button}) => (
<>
<img className="img___shop" key={id} src={img}></img>
<div key={id}>
<button onClick={() => addCart(id)}>{button}</button>
</div>
</>
))}
</div>
</div>
)
}
export default Shop
my data file:
import diCaprio from './img/diCaprio.jpg'
import steveJobs from './img/steveJobs.jpg'
import lips from './img/lips.jpg'
import buda from './img/buda.jpg'
import spaceDog from './img/spaceDog.jpg'
import astroNube from './img/astroNube.jpg'
import banksy from './img/Banksy.jpg'
import banksyDJ from './img/banksyDJ.jpg'
var uniqid = require('uniqid');
const data = [{
id: uniqid(),
title: "Steve Jobs",
img: steveJobs,
homeImg: steveJobs,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img: diCaprio,
homeImg: diCaprio,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img: lips,
homeImg: lips,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img: buda,
homeImg: buda,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img: spaceDog,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img:astroNube,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img: banksy,
button: "add to cart",
count: 0
},
{
id: uniqid(),
img:banksyDJ,
button: "add to cart",
count: 0
}
]
export default data;
includes will not work with objects. try find or findIndex instead.
Also I think your check is wrong. You need to check if items has the selected productId and if yes, then update its count.
const addCart = (productsId) => {
setCount(count + 1)
data.forEach((product) => {
let index = items.findIndex(itm => itm.id === product.id && productId === product.id)
if (index >= 0) {
let newProduct = { ...items[index] }
newProduct.count += 1
setItem( [...item, [index]: newProduct ])
} else {
setItem(item.concat(product))
}
})
}
Since product is an object, includes will not work as it only do "shallow" comparison like if you have a simple array (i.e ['a', 'b', 'c'].includes('c');
In your case you need to "find" the product by id to know if it exist.
const addCart = (productsId) => {
setCount(count + 1)
data.forEach((product) => {
const exist = item.find(i => i.id === product.id); // add this one
if (exist) {
product.count += 1
} else if (product.id === productsId) {
setItem(item.concat(product))
}
})
}
You can learn more about array.find here.

List childs are not beeing updated correctly? (React / Preact)

I have the following component
import {h, Component} from 'preact'
import {getPersons} from '../../lib/datalayer'
import Person from '../person'
import {SearchInput} from '../search'
export default class Persons extends Component {
state = {
allPersons: [],
persons: [],
search: ''
}
async fetchData () {
try {
const allPersons = await getPersons()
this.setState({allPersons: allPersons.slice(), persons: allPersons.slice()})
} catch (error) {
....
}
}
constructor (props) {
super(props)
this.state = {
allPersons: [],
persons: [],
search: ''
}
this.fetchData()
}
onSearchInput = (search) => {
if (search === '') {
this.setState({search: search, persons: this.state.allPersons.slice()})
} else {
const persons = this.state.allPersons.filter(p => p.name.toLowerCase().includes(search.toLowerCase())).slice()
this.setState({search: search, persons: persons)})
}
}
render () {
const {persons} = this.state
return (
<div>
<SearchInput onInputChange={this.onSearchInput} placeHolder={'filter: name'} />
{persons.map(p => <Person person={p} />)}
</div>
)
}
}
The page renders a list of Persons and it has a filter on top. The filter seems to work fine, I tested it by doing a console.log of the results are just fine
The problem is that, if my list contains the objects:
[{name: 'thomas'}, {name: 'john'}, {name: 'marcus'}, {name: 'usa'}]
And I write in the search input: 'us'
The filter works fine and the result is:
[{name: 'marcus'}, {name: 'usa'}] \\ (the expected result)
In the page this objects are rendered
[{name: 'thomas'}, {name: 'john'}] \\ (wrong, this are the two first elements of the list)
If I search: 'joh'
The filter's result is
[{name: 'john'}] \\ (this is fine)
And the page renders only
[{name: 'thomas'}] \\ (the first element in the list)
It looks like the amount of elements that are rendered it's fine, but the content of the childs of the list is not beeing re-rendered.
Whats's wrong with my code?
React uses keys on the children of a list to determine which items changed and which of them remains the same. Since you have not specified a key on person, it takes index to be the key.
When index is key, you can see how shortening the list to two items, shows up the first two items in the list (the other indices are now missing). To get around this, you have to give a unique identifier on the person as key.
From your object, assuming name is unique (it usually isn't):
{persons.map(p => <Person person={p} key={p.name} />)}
Why are keys necessary - Docs
I cannot reproduce the error with react, did remove some unneeded slice and added unique id to each element (React will complain if you do not give each element a unique key and maybe so will preact).
const Person = React.memo(props => (
<pre>{JSON.stringify(props, undefined, 2)}</pre>
));
class Persons extends React.Component {
state = {
allPersons: [
{ name: 'aaa', id: 1 },
{ name: 'aab', id: 2 },
{ name: 'abb', id: 3 },
{ name: 'bbb', id: 4 },
{ name: 'bbc', id: 5 },
],
persons: [
{ name: 'aaa', id: 1 },
{ name: 'aab', id: 2 },
{ name: 'abb', id: 3 },
{ name: 'bbb', id: 4 },
{ name: 'bbc', id: 5 },
],
search: '',
};
onSearchInput = search => {
if (search === '') {
//slice not needed here
this.setState({
search: search,
persons: this.state.allPersons,
});
} else {
//filter already copies allPersons
const persons = this.state.allPersons.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
);
this.setState({ search: search, persons: persons });
}
};
render() {
const { persons } = this.state;
return (
<div>
<input
type="text"
value={this.state.search}
onChange={e => this.onSearchInput(e.target.value)}
placeHolder={'filter: name'}
/>
{persons.map(p => (
<Person person={p} key={p.id} />
))}
</div>
);
}
}
ReactDOM.render(
<Persons />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Copy object from one array of objects to another in React

I have this.props.person like this:
person = [{id: 1, name: John, age: 20},
{id: 2, name: Kate, age: 30},
{id: 3, name: Mike, age: 25}]
And I have empty this.state.newPerson: [].
Function get id value, searching object with this id in this.props.person, and add to this.state.newPerson this object. It can be repeats a few times.
For example: I call funcion addPerson twice with id=1 and id=3. In result I should get
this.state.newPerson: [{id: 1, name: John, age: 20}, {id: 3, name: Mike, age: 25}].
I tried:
addPerson(idPerson) {
const list = this.state.newPerson;
const personToList = this.props.person.find(el => el.id === idPerson);
const newP = Object.assign(list, { personToList });
this.setState({ newPerson: newP });
}
In fact, I got something like [personToList {id: ...}]
How can I fix it?
why do you use Object.assign if this.state.newPerson is an array?
Just use
list.push(personToList) but set your state like this this.state.newPerson = [];
You want to add personToList to the list array instead of assigning the entire array with the object { personToList: personToList }.
You could use the spread syntax instead.
Example
class App extends React.Component {
state = { newPerson: [] };
addPerson = personId => {
const list = this.state.newPerson;
if (list.some(el => el.id === personId)) {
return;
}
const personToList = this.props.person.find(el => el.id === personId);
const newP = [...list, personToList];
this.setState({ newPerson: newP });
};
render() {
return (
<div style={{ display: "flex" }}>
<div>
{this.props.person.map(p => (
<div id={p.id} onClick={() => this.addPerson(p.id)}>
{p.name}
</div>
))}
</div>
<div>
{this.state.newPerson.map(p => (
<div id={p.id} onClick={() => this.addPerson(p.id)}>
{p.name}
</div>
))}
</div>
</div>
);
}
}
ReactDOM.render(
<App
person={[
{ id: 1, name: "John", age: 20 },
{ id: 2, name: "Kate", age: 30 },
{ id: 3, name: "Mike", age: 25 }
]}
/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Object.assign is not right for this use case scenario. You're trying to add to an array on the state.
Try this instead:
addPerson(idPerson) {
let list = [...this.state.newPerson];
let personToList = this.props.person.find(el => el.id === idPerson);
list.push(personToList);
this.setState({
newPerson: list
});
}
Object.assign is used to combine two javascript object.personToList is already an object.
const newP = Object.assign(list, personToList);
Actually,you could use push to fix it.
const newP = list.push(newP)
find returns only the first matching item. Use filter to get all of them.
Check the documentation:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
try this:
addPerson(idPerson) {
this.setState({
newPerson : this.state.newPerson.concat(this.props.person.find( i => i.id === idPerson))
})
}
}
or
addPerson(idPerson) {
this.setState({
newPerson : [this.state.newPerson, this.props.person.find( i => i.id === idPerson)]
})
}

Categories

Resources