I'm working on some code where I fetch some items in onMount then render them. I don't want to play a ton of transitions for the big batch of fetched items. However, I do want to play transitions when individual items are added or removed from the list. This is a similar situation to this question.
If I write this, then I get the transition behavior I want.
{#if itemsFetched}
{#each items as it (it.id)}
<div transition:slide|local>
<span>{it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{/each}
{/if}
However, inside of the each block, I need to render the items 1 of 2 ways. For this example, I render even IDs one way and odd IDs another. If I add an if block, then the individual item transitions don't play at all.
{#if itemsFetched}
{#each items as it (it.id)}
{#if it.id%2 === 0}
<div transition:slide|local>
<span>render like this {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{:else}
<div transition:slide|local>
<span>render this other way {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{/if}
{/each}
{/if}
Here's a full example.
Sandbox: https://codesandbox.io/s/crazy-heyrovsky-bwviny?file=/App.svelte
<script>
import { slide } from "svelte/transition";
import { onMount } from "svelte";
// Initially we have no items.
let items = [];
let id = 0;
let itemsFetched = false;
onMount(() => {
// Fetch items from API.
items = [
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
{id: id, name: `item ${id++}`},
];
itemsFetched = true;
});
function addItem() {
items = [
...items,
{id: id, name: `item ${id++}`},
];
}
function removeItem(rmIt) {
return () => {
items = items.filter(it => it.id !== rmIt.id);
};
}
</script>
<div>
<button on:click={addItem}>add</button>
{#if itemsFetched}
{#each items as it (it.id)}
{#if it.id%2 === 0}
<div transition:slide|local>
<span>render like this {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{:else}
<div transition:slide|local>
<span>render this other way {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{/if}
{/each}
{/if}
</div>
Why does adding an if block break the local transition? If I remove local, then the items play transitions when added or removed, but then I lose the initial onMount behavior I want.
local restricts the transition to the creation/destruction of the block the element is in, which is the {#if it.id%2 === 0}. But since the ID presumably never changes, the block just always exists, i.e. there is no change and hence no animation is triggered.
Put the {#if} inside the element with the transition.
Instead of this:
{#each items as it (it.id)}
{#if it.id%2 === 0}
<div transition:slide|local>
<span>render like this {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{:else}
<div transition:slide|local>
<span>render this other way {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
</div>
{/if}
{/each}
Do this:
{#each items as it (it.id)}
<div transition:slide|local>
{#if it.id%2 === 0}
<span>render like this {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
{:else}
<span>render this other way {it.id} {it.name}</span>
<button on:click={removeItem(it)}>remove</button>
{/if}
</div>
{/each}
Related
I have two files that return html fragments. They are identical except for the image. The fact is that the server has different paths to the playlist image and the genre image.
props.items.images[0].url
and
props.items.icons[0].url
because of this, I had to distribute this code to two different files. How could I combine them into one?
const Playlist = props => {
const playListClick = e => {
props.onClick(e.target.title);
}
return (
<section id={props.selectedValue} className="column column__hidden" onClick={playListClick}>
<a className="link__decoration link__track hover__track link__one" href="#">
<div>
{props.items.map((item, idx) =>
<div className="container" key={idx + 1} title={item.id} >
<div className="content__track" title={item.id}>
<img className="img__tarck" title={item.id} src={item.images[0].url}/>
<div className="name" title={item.id}>{item.name}</div>
</div>
</div>)}
</div>
</a>
</section>
);
}
const Genre = props => {
const genreClick = e => {
props.onClick(e.target.title);
}
return (
<section id={props.selectedValue} className="column column__hidden" onClick={genreClick}>
<a className="link__decoration link__track hover__track link__one" href="#">
<div>
{props.items.map((item, idx) =>
<div className="container" key={idx + 1} title={item.id}>
<div className="content__track" title={item.id}>
<img className="img__tarck" title={item.id} src={item.icons[0].url}/>
<div className="name" title={item.id}>{item.name}</div>
</div>
</div>)}
</div>
</a>
</section>
);
You can use a Conditional (ternary) Operator to look if icons exists and if not fallback to using the image. Further assistance is hard due to not knowing the surrounding circumstances.
<img className="img__tarck" title={item.id} src={item.icons ? item.icons[0].url : item.images[0].url}/>
I have loaded a data from API and displayed here with VueJS. I have users information inside users[] array. I also have users with two types of plan: basic_plan and standard_plan. Currently it shows all users.
Now I want to apply filters equally to this example: https://codepen.io/marn/pen/jeyXKL?editors=0010
I also got an error filter not defined
Filters:
<input type="radio" v-model="selectedItems" value="All" /> All
<input type="radio" v-model="selectedItems" value="basic_plan" /> Basic
<ul
v-for="(user, index) in selectedUser.data"
:key="index"
class="watchers divide-y divide-gray-200"
>
<li class="py-4">
<div class="mx-4 flex space-x-3">
<span
class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500"
>
</span>
<div class="flex-1 space-y-1">
<h3 class="text-sm font-medium">
{{ user.name }}
</h3>
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-500">
{{ user.plan }}
</p>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<script
export default {
data() {
return {
users: [],
selectedItems:"All"
};
},
created() {
this. users();
},
methods: {
users {
axios
.get('api/users')
.then(response => {
this.users = response.data;
}
},
computed: {
selectedUser: function() {
if(this.selectedItems ==="All"){
return this.users
}else{
return this.users.data.filter(function(item) {
console.log(item)
return item.plan === this.selectedItems;
});
}
}
}
};
</script>
when All is selected vue dev tool shows this
selectedUser:Object //OBJECT SHOWING
data:Array[10]
links:Object
meta:Object
but when basic radio is selected vue shows this
selectedUser:Array[1] //ARRAY SHOWING
0:Object
price:"10"
plan:"basic_planl"
If you want to filter out specific users you must apply the "filter" function to the users variable like this:
this.users.filter(...)
With this function you then can filter the users based on their plan like this:
this.users.filter((user) =>
user.plan === this.selectedItems;
});
For a modern approach I used an arrow function. And without using curly brackets the statement inside the function is returned by default, so that's why there is no "return" statement.
Try this way instead, as you are already using v-for in your HTML, you can conveniently filter out users without any more loops, if you are getting value as "basic_plan" in the "user.plan" key.
Also, I think that you should move your v-for to <li> tag instead of <ul> along with the validation on <ul> if there are no users in the array.
<template>
<div>
<input type="radio" v-model="selectedItems" value="All" /> All
<input type="radio" v-model="selectedItems" value="basic_plan" /> Basic
<ul v-if="selectedUser.data.length" class="watchers divide-y divide-gray-200">
<li v-for="(user, index) in selectedUser.data" :key="index" class="py-4">
<div v-if="filterUser(user)" class="mx-4 flex space-x-3">
<span class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500"></span>
<div class="flex-1 space-y-1">
<h3 class="text-sm font-medium">
{{ user.name }}
</h3>
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-500">
{{ user.plan }}
</p>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
users: [],
selectedItems:"All"
};
},
methods: {
filterUser(user){
if(this.selectedItems === 'All'){
return true;
}
if(this.selectedItems === 'basic_plan'){
return this.selectedItems === user.plan;
}
}
},
}
</script>
Here's what I've have so far - Full working view https://codesandbox.io/s/hungry-elbakyan-v3h96
Accordion component:
const Accordion = ({ data }) => {
return (
<div className={"wrapper"}>
<ul className={"accordionList"}>
{data.map((item) => {
return (
<li className={"accordionListItem"} key={item.title}>
<AccordionItem {...item} />
</li>
);
})}
</ul>
</div>
);
};
const AccordionItem = ({ content, title }) => {
const [state, setState] = useState({
isOpened: false
});
return (
<div
className={cn("accordionItem", state.isOpened && "opened")}
onClick={() => setState({ isOpened: !state.isOpened })}
>
<div className={"lineItem"}>
<h3 className={"title"}>{title}</h3>
<span className={"icon"} />
</div>
<div className={"inner"}>
<div className={"content"}>
<p className={"paragraph"}>{content}</p>
</div>
</div>
</div>
);
};
When I click on the accordion item nothing happens. I can see the content appears in inspect and the state changes as expected but it doesn't slide down. Did I miss something in my css or component?
Here is what I was able to achieve. You may not like it completely(animations). But things seems sorted
https://codesandbox.io/s/relaxed-babbage-2zt4f?file=/src/styles.css
props name was not right for accordion body
and styles need to be changes once the accordion is in open state.
You need to add or remove the className inner when state.isOpen so you can see your content
Newbie dev learning React.
I'm trying to create an upvote functionality to a blog post in React but when I click on the upvote button I'm upvoting all of the blog post cards at once instead of the individual card.
How can I fix this? I believe the issue may be in the way I'm setting setState? But I may be wrong and looking for help.
Thanks in advance!
====
class Posts extends Component {
state= {
points: 0
}
componentDidMount() {
this.props.fetchPosts()
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.newPost) {
this.props.posts.unshift(nextProps.newPost);
}
}
handleClick = () => {
this.setState({points: this.state.points + 1})
}
render() {
const postItems = this.props.posts.map((post, index) => (
<div key={index} className="ui three stackable cards">
<div className="ui card">
<div className="content">
<div className="header">{post.title}</div>
<div className="meta"> {post.author}</div>
<div className="description">
<p>{post.body}</p>
</div>
</div>
<div className="extra content">
<i className="check icon"></i>
{this.state.points} Votes
</div>
<button className="ui button"
type="submit"
onClick={this.handleClick}>Add Point</button>
</div>
</div>
))
return (
<div>
<br />
<h2 className="ui header">
<i className="pencil alternate icon"></i>
<div className="content">
Blog Feed
<div className="sub header">Create New Post!</div>
</div>
</h2>
{postItems}
</div>
)
}
}
You have a single component storing the "points" state for all your posts. To achieve the functionality you described, each post should be it's own component with it's own state.
class Post extends Component {
state = {
points: 0
}
handleClick = () => {
this.setState({points: this.state.points + 1})
}
render = () =>
<div key={index} className="ui three stackable cards">
<div className="ui card">
<div className="content">
<div className="header">{this.props.title}</div>
<div className="meta"> {this.props.author}</div>
<div className="description">
<p>{this.props.body}</p>
</div>
</div>
<div className="extra content">
<i className="check icon"></i>
{this.state.points} Votes
</div>
<button className="ui button"
type="submit"
onClick={this.handleClick}>Add Point</button>
</div>
</div>
}
}
You are upvoting every card because you have only one counter. A separate counter should be defined for every card.
state = {}; // dictionary-a-like state structure
handleClick = (id) => () => {
this.setState((prevState) => ({
[id]: prevState[id] ? prevState[id] + 1 : 1, // check and increment counter
}));
}
onClick={this.handleClick(post.id)} // call function with post.id as argument
{this.state[post.id] || 0} Votes // display votes for every card
Note: I assumed that every card has it's own unique id, if not - index may come handy too.
You will need one counter for each post. Currently you only have a single counter for all posts, which means that they all display that same value.
The best way to achieve this would probably be to separate your post into its own component, and have that keep track of the counter.
The following solution uses a post ID (if you have it) to create a key in a stateful points object. Then, on click, you can add to the correct points key.
state = {
points: {}
}
handleClick = postId => {
this.setState({
points: {
...this.state.points,
[postId]: (this.state.points[postId] || 0) + 1
}
})
}
const postItems = this.props.posts.map((post, index) => (
...
<div className="extra content">
<i className="check icon"></i>
{this.state.points[post.id] || 0} Votes
</div>
<button
className="ui button"
type="submit"
onClick={() => this.handleClick(post.id)}
>
Add Point
</button>
...
)
I am trying to get the value the user inputs into the modal input boxes and then add them to my state array. I have tried to take the value from the inputs and then push them into a clone of my state array and then set the state to the clone. However, this approach does not seem to work. I would appreciate if anyone could chime in.
var Recipes = React.createClass({
// hook up data model
getInitialState: function() {
return {
recipeList: [
{recipe: 'Cookies', ingredients: ['Flour', 'Chocolate']},
{recipe: 'Pumpkin Pie', ingredients: ['Pumpkin Puree', 'Sweetened Condensed Milk', 'Eggs', 'Pumpkin Pie Spice', 'Pie Crust']},
{recipe: 'Onion Pie', ingredients: ['Onion', 'Pie-Crust']},
{recipe: 'Spaghetti', ingredients: ['Noodles', 'Tomato Sauce', 'Meatballs']}
]
}
},
ingredientList: function(ingredients) {
return ingredients.map((ingredient, index) => {
return (<li key={index} className="list-group-item">{ingredient}</li>)
})
},
eachRecipe: function(item, i) {
return (
<div className="panel panel-default">
<div className="panel-heading"><h3 key={i} index={i} className="panel-title">{item.recipe}</h3></div>
<div className="panel-body">
<ul className="list-group">
{this.ingredientList(item.ingredients)}
</ul>
</div>
</div>
)
},
add: function(text) {
var name = document.getElementById('name').value;
var items = document.getElementById('ingredients').value.split(",");
var arr = this.state.recipeList;
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr})
},
render: function() {
return (
<div>
<div>
<button type="button" className="btn btn-success btn-lg" data-toggle="modal" data-target="#myModal">Add Recipe</button>
<div id="myModal" className="modal fade" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal">×</button>
<h4 className="modal-title">Add a new recipe</h4>
</div>
<div className="modal-body">
<form role="form">
<div className="form-group">
<label forName="recipeItems">Recipe</label>
<input ref="userVal" type="recipe" className="form-control"
id="name" placeholder="Enter Recipe Name"/>
</div>
<div className="form-group">
<label for="ingredientItems">Ingredients</label>
<input ref="newIngredients" type="ingredients" className="form-control"
id="ingredients" placeholder="Enter Ingredients separated by commas"/>
</div>
<button onClick={this.add} type="submit" className="btn btn-default">Submit</button>
</form>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{
this.state.recipeList.map(this.eachRecipe)
}
</div>
</div>
);
}
});
ReactDOM.render(
<Recipes />,
document.getElementById('master')
)
The problem is that whenever you click the button the form is submitted and the page reloads.
One solution is to take the onClick={this.add} out of the button and add onSubmit={this.add} at the <form> tag instead.
So, at the add() function you do:
add: function(e) {
e.preventDefault();
var form = e.target;
var name = form.name.value;
var items = form.ingredients.value.split(",");
var arr = this.state.recipeList;
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr});
},
First, you call e.preventDefault() so your form won't reload the page. Second, you could use the target to access the input value through theirs names attribute and set the state.
A few things that can be improved in your code.
add: function(text) {
...
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr})
}
By using the push() method on your array you are essentially modifying the component's state by pushing the new item into it. It's strongly advised that you don't mutate your React component's state directly without using the setState method.
A way to fix this would be by creating a new array in which you will copy all of the existing recipes plus the new recipe you are adding.
If you are using ES6/2015 it's even easier to achieve this:
add: function(text) {
...
var newRecipe = {
recipe: name,
ingredients: items
};
this.setState({ recipeList: [...this.state.recipeList, newRecipe] });
}
That way you don't modify the component's state before calling setState() method thus keeping it intact.
Next
add: function() {
var name = document.getElementById('name').value;
var items = document.getElementById('ingredients').value.split(",");
...
}
Try as much as possible to use the Refs (references) to access your input nodes, that way you don't have to use getElementById because it is more in line with the rest of your react code.
add: function(text) {
var name = this.refs.userVal.value;
var items = this.refs.newIngredients.value.split(",");
...
},
render: function() {
return (
<div>
...
<input
ref="userVal"
placeholder="Enter Recipe Name"
/>
<input
ref="newIngredients"
placeholder="Enter Ingredients separated by commas"
/>
...
</div>
);
});
And lastly
To prevent the Button element from submitting the form and taking you to another page, which is what happening here, you could set the button's type attribute to button and it should no longer submit the form. By default a button element is of type submit.
<button onClick={this.add} type="button" className="btn btn-default">Submit</button>
And so by doing this you won't need to "prevent" the default action from occurring (which is to submit the form), using the Event.preventDefault() method in your onClick function handler.
Here is a jsBin link with the above changes you could look at.