When I run this code, I get a bootstrap panel group for each recipe item inside of local storage. When I try to delete a recipe, sometimes the right recipe is removed and sometimes not. The console shows that the right recipe is removed from local storage but for some reason when the app component resets its state, the wrong recipe is removed. I've noticed that if I try to delete the recipes from the bottom up, it works. But if I click on the first recipe, the bottom recipe is removed.
I know this is an easy fix but I need a fresh perspective. Thanks everyone!
Also, sorry lack of indentation in the code - stack overflow wasn't being too friendly with the spacing
class App extends Component {
constructor() {
super();
this.deleteRecipe = this.deleteRecipe.bind(this)
this.state = {
recipeData: JSON.parse(localStorage.getItem('recipeData'))
}
}
deleteRecipe() {
this.setState({recipeData: JSON.parse(localStorage.getItem('recipeData'))})
}
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React Recipe Box!</h2>
</div>
<div className="container">
{this.state.recipeData.map(recipe => {
return (
<Recipe name={recipe.name} ingredients={recipe.ingredients} deleteRecipe={this.deleteRecipe}/>
)
})}
</div>
</div>
);
}
}
class Recipe extends Component {
constructor() {
super();
this.onDeleteRecipe = this.onDeleteRecipe.bind(this)
}
componentWillMount(){ //set state when component is about to mount
this.state = {
name: this.props.name,
ingredients: this.props.ingredients,
}
}
onDeleteRecipe() {
var recipeList = JSON.parse(localStorage.getItem('recipeData'));
for(var i = 0; i < recipeList.length; i++) {
if(recipeList[i].name === this.state.name) {
recipeList.splice(i, 1);
console.log("Deleted " + this.state.name, recipeList);
localStorage.removeItem('recipeData');
localStorage.setItem('recipeData', JSON.stringify(recipeList));
this.props.deleteRecipe();
}
}
}
render() {
return (
<div>
<div className="panel-group">
<div className="panel panel-primary">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-target={'#' + (this.state.name).replace(/\s/g, '')} href={'#' + (this.state.name).replace(/\s/g, '')}>
{this.state.name}
</a>
</h2>>
</div>
<div id={(this.state.name).replace(/\s/g,'')} className="panel-collapse collapse">
<div className="panel-body">
{this.state.ingredients.map(ingredient => {
return <li className="list-group-item">{ingredient}</li>
})}
<div className="btn-group">
<button className="btn btn-sm btn-info" data-toggle="modal"
data-target={'#' + (this.state.name).replace(/\s/g, '') + 'EditModal'}>Edit</button>
<button className="btn btn-sm btn-danger" data-toggle="modal"
data-target={'#' + (this.state.name).replace(/\s/g, '') + 'RemoveModal'}
>Delete</button>
</div>
</div>
</div>
</div>
</div>
<div className="modal modal-lg" id={(this.state.name).replace(/\s/g, '') + 'EditModal'} >
<div className="modal-content">
<div className="modal-header">
<h2>Edit {this.state.name}</h2>
</div>
<div className="modal-body">
<ul className="list-group list-unstyle">
{this.state.ingredients.map( ingredient => {
return <li className="list-group-item">{ingredient}</li>
})}
</ul>
</div>
<div className="modal-footer">
<div className="btn-group">
<button className="btn btn-sm btn-info" data-dismiss="modal">Save</button>
<button className="btn btn-sm btn-danger" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div className="modal modal-lg" id={this.state.name.replace(/\s/g, '') + 'RemoveModal'}>
<div className="modal-content">
<div className="modal-body">
<h3>This will remove the selected recipe. Are you sure?</h3>
</div>
<div className="modal-footer">
<div className="btn-group">
<button className="btn btn-sm btn-danger" data-dismiss="modal" onClick={this.onDeleteRecipe}>Delete</button>
<button className="btn btn-sm btn-info" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default App;
I'm still a novice, but... I built a react recipe box for FreeCodeCamp a few months ago, so I just pulled it up to compare my delete function to yours.
I notice that you treat localStorage differently than I did in mine. (Not that my way is best or even right!) I wonder if somehow the problem is rooted in that design. Your delete function goes into localStorage and makes changes, then you run a setState to sort of "re-get" the recipeData, if I'm reading right?
By contrast, my app declares a variable from localStorage and I set this.state.recipeArray to equal that variable. Then all of my edit/add/delete functions change this.state.recipeArray, not localStorage. Sorta like this:
handleDelete: function(e) {
e.preventDefault();
var replacementRecipeArray = this.state.recipeArray.filter((recipe) => recipe.title !== e.target.name);
//dot-filter with ES6 fat-arrow returns all items where title doesn't match the one we clicked; IOW, it removes the one we want to delete
this.setState({
recipeArray: replacementRecipeArray
});
}
In order to get any changes to this.state.recipeArray back to localStorage, I do a localStorage.setItem every time I render the page.
render() {
//first save current array to localstorage - wasn't reliable anywhere else
localStorage.setItem("_shoesandsocks_recipes", JSON.stringify(this.state.recipeArray));
//render page
return (
<div className="App">
<div className="recipeContainer">
// etc etc
For all I know this is a crazy design, but it works.
Related
I'm working on a small project in vue.js connected to a lumen API (working).
I currently have a list of students ('Etudiants') in which I can click in the list to select one, and delete it via a button in a toolbar.
When a student is deleted I'm reloading the student list (since it's not up to date anymore), therefore I'm doing 2 api calls via axios.
DELETE http://www.url.com/etudiants (param: idEtudiant)
GET http://www.url.com/etudiants (param: page)
The problem is that my API calls are not done in the right order, as shown here on a screenshot of the calls (with watterfall):
This problem involves 3 vue files.
'Etudiants.vue' and its 2 childs: 'ListeEtudiants.vue' (student list) and 'BarreOutilsEtudiant.vue' (toolbar)
This simple image shows the hierarchy of the 3 files and the order of which everything should execute.
In my case (when it's not working) the action number 3, the axios DELETE, happens in last.
Here are the contents of my files:
Etudiants.vue:
<template>
<div id="etudiants" class="container-fluid h-100">
<div class="row">
<div class="col-3 borderR">
<ListeEtudiants ref="list" #idEtudiantChanged="updateIdEtudiant"/>
</div>
<div class="col-9 bg-light">
<BarreOutilsEtudiant v-if="idEtudiant != null" :idEtudiant="idEtudiant" #delEtudiant="delEtudiant"/>
<InfosEtudiant v-if="idEtudiant != null" :idEtudiant="idEtudiant"/>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import ListeEtudiants from '#/components/ListeEtudiants.vue'
import InfosEtudiant from '#/components/InfosEtudiant.vue'
import BarreOutilsEtudiant from '#/components/BarreOutilsEtudiant.vue'
export default {
components: {
ListeEtudiants,
InfosEtudiant,
BarreOutilsEtudiant
},
data: function(){
return {
idEtudiant: null
}
},
methods:{
updateIdEtudiant(idEtudiant){
this.idEtudiant=idEtudiant;
},
delEtudiant(){
axios
.delete('http://82ab2617.ngrok.io/etudiants', {params: {"idEtudiant" :this.idEtudiant}})
.then(this.$refs.list.loadList())
.catch(error => console.log(error));
}
}
}
</script>
ListeEtudiants.vue:
<template>
<div id="ListeEtudiants">
<div class="row bg-light">
<!-- Trigger Modal Ajout Etudiant -->
<button class="btn btn-light w-100" data-toggle="modal" data-target="#addModal">
<font-awesome-icon icon="plus" size="1x"/>
<span> Ajouter un Etudiant</span>
</button>
</div>
<ul v-if="etudiants != null" id="list" class="row flex-nowrap list-group list-group-flush pr-0">
<button v-for="etudiant in etudiants.data" v-on:click='$emit("idEtudiantChanged",etudiant.idEtudiant)' class="btn btn-light text-left list-group-item pl-5 py-1">{{ etudiant.nom }} {{ etudiant.prenom }}</button>
</ul>
<ul v-else class="row flex-nowrap list-group list-group-flush pr-0">
</ul>
<div class="row bg-light">
<button class="btn btn-light col-3" v-on:click="page -= 1" :disabled="page === 1 || disabled"><font-awesome-icon icon="chevron-left" size="1x"/></button>
<div class="align-middle col-6 my-auto">{{ page }} / {{ maxPage }}</div>
<button class="btn btn-light col-3" v-on:click="page += 1" :disabled="page === maxPage || disabled"><font-awesome-icon icon="chevron-right" size="1x"/></button>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: "ListeEtudiants",
data: function(){
return {
etudiants: null,
maxPage:1,
disabled:false,
page:1,
}
},
methods: {
parseAndDisplay: function(data){
this.etudiants = data;
this.maxPage = data.last_page;
this.page = data.current_page;
this.disabled = false;
},
loadList: function(){
this.disabled = true;
this.etudiants = null;
axios
.get('http://82ab2617.ngrok.io/etudiants', {params: {page:this.page}})
.then(response =>this.parseAndDisplay(response.data))
.catch(error => console.log(error));
}
},
watch: {
'page': function(newVal, oldVal){
if((newVal === 0 && oldVal === 1) || (newVal === this.maxPage+1 && oldVal === this.maxPage)){
this.page = oldVal;
}else{
if(oldVal !== 0 && oldVal !== this.maxPage+1) {
this.loadList();
}
}
}
}
,
mounted() {
this.loadList();
}
}
</script>
BarreOutilsEtudiant.vue:
<template>
<div class="row p-2 navbar-expand navbar-info bg-info">
<button class="btn btn-info mr-5" type="button"><font-awesome-icon icon="download" size="1x"/> Télécharger le Bulletin</button>
<button class="btn btn-info ml-auto" type="button"><font-awesome-icon icon="user-edit" size="1x"/></button>
<button class="btn btn-danger ml-4" v-on:click="$emit('delEtudiant')" type="button"><font-awesome-icon icon="trash-alt" size="1x"/></button>
</div>
</template>
<script>
export default {
name: "BarreOutilsEtudiant"
}
</script>
<style scoped>
</style>
Thank you very much for helping me.
I believe the problem is here:
.then(this.$refs.list.loadList())
That will call loadList immediately and pass the value it returns to then, which isn't what you want.
Instead it should be something like this, wrapping it in a function:
.then(() => this.$refs.list.loadList())
I am facing a problem in deleting item from an array. Array splice supposed to work but its not working like I want. Its always delete the item from last. I am using Vue.js . I am pushing item dynamically to an array. But after click remove its delete from the last. why I am facing this. I am attaching the codes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import flowButton from '../assets/flow-button'
export default {
name: "textArea",
props:{
index : Number
},
data() {
return {
buttons : [],
btnClass : 'badge-primary',
}
}
components : {
flowButton
},
methods : {
addBtn () {
if(this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if(this.buttons.length < 3) {
this.buttons.push({
title : ''
});
}
},
remove(index) {
this.buttons.splice(index, 1)
}
}
}
</script>
This must be because of your flow-button I have tried to replicate your error but endup to this code. I just replaced the flow-button with input and it works. Try the code below.
Use v-bind:key="index", When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index"
You have missing comma between data and components, I remove the component here it won't cause any error now, and more tips don't mixed double quotes with single qoutes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons" v-bind:key="index">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o">sdfsdff</i></div>
<input type="text" v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'textArea',
props: {
index: Number
},
data () {
return {
buttons: [],
btnClass: 'badge-primary'
}
},
methods: {
addBtn () {
if (this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if (this.buttons.length < 3) {
this.buttons.push({
title: ''
})
}
},
remove (index) {
this.buttons.splice(index, 1)
}
}
}
</script>
I think that you may be facing a conflict with the index prop of your component. Try to use a different name for the index of your v-for loop:
<div v-for="(item, ind) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(ind)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
Try this. Removing an item correctly using this.
<div v-for="(item, ind) in buttons" :key="JSON.stringify(item)">
I'm creating so called recipe box, where you should be able to add/edit/delete recipes. Initial rendering part seems to be working fine, but I'm struggling when it comes to states and updating html depending what was changed: whether existing recipe was modified, deleted or new one added.
Currently I implemented state change trigger when recipe is edited. By reading various articles I came to conclusion that if you want to read values from another element when some other element is interacted (in my case from input element when button element is clicked), I need to add state to track input directly while it is typed and then use that state to trigger what I want (In my case I just use value from so called pending state and set to normal state when that button is pressed).
But it seems it is not working. Though I'm probably doing something wrong.
Here is the part I implemented states I talked about:
class RecipeComponent extends React.Component {
constructor(props){
super(props);
this.state = {
title: '',
pendingTitle: '',
ingredients: '',
pendingIngredients: '',
}
}
handleChange(e, key){
let obj = {};
obj[key] = e.target.value;
this.setState(obj);
}
handleClick(){
this.setState(
{title: this.pendingTitle, ingredients: this.pendingIngredients});
}
_renderModal(target, ctx){
return (
<div className="modal fade" id={target} 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">{ctx.title}</h4>
</div>
<div className="modal-body">
<div className="form-group">
<label htmlFor="title" className="control-label"><span>Recipe</span></label>
<input type="text" id="title" className="form-control" placeholder="Recipe Name" defaultValue={ctx.recipeTitle ? ctx.recipeTitle : ''}
onKeyUp={(e) => this.handleChange(e, 'pendingTitle')}
/>
</div>
<div className="form-group">
<label htmlFor="ingredients" className="control-label"><span>Ingredients</span></label>
<input type="text" id="ingredients" className="form-control" placeholder="Enter Ingredients, separated by commas" defaultValue={ctx.ingredients ? ctx.ingredients : ''}
onChange={(e) => this.handleChange(e, 'pendingIngredients')}
/>
</div>
</div>
<div className="modal-footer">
{/*Seems to not update state properly*/}
<button type="button" className="btn btn-primary" onClick={() => this.handleClick()} data-dismiss="modal">{ctx.title}</button>
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
}
...
...
{/*Here title from state is never set*/}
// Should this.state.title replace default title?
recipeTitle: this.state.title || recipe.title,
}
Full code can be found here (you can also test how it is currently working if it was hard to understand what I meant. Try to open any recipe, edit it and press button Edit Recipe and nothing will happen, recipe title will not be changed): https://codepen.io/andriusl/pen/LjxYQo
You directly accessed this.pendingTitle instead of this.state.pendingTitle. so change this to
handleClick(){
this.setState(
{title: this.state.pendingTitle, ingredients: this.state.pendingIngredients});
}
and change this code to
<a data-toggle="collapse" data-target={anchor_target}
href={anchor_target} className="collapsed">{this.state.title||recipe.title}
I have created additional submit modal in order to receive user confirmation after he decides to delete the post from collection and I cant figure out how to target the post.
Another thing that I would like to ask you is a productivity question, is it wise to insert DeletePost component into each post component or there is a way to have it inserted inside currentPage component and somehow bind the modal call to to the post.
Here is the code for DeletePost component:
class DeletePost extends Component {
handleDelete(event) {
event.preventDefault();
Meteor.call('posts.remove', post);
$('#modalDelete').modal('hide');
}
render() {
return (
<div className="modal fade form-delete" id="modalDelete" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div className="form-outer">
<form id='delete_post' onSubmit={this.handleDelete.bind(this)}>
<div className='form-text form-header'>
<p>My dear, <strong>master</strong></p>
<p>Are you really sure about that?</p>
</div>
<button type="button" className="form-button button-delete" data-dismiss="modal">No</button>
<button type="sumbit" className="form-button button-delete">Yes</button>
</form>
</div>
</div>
);
}
}
And here is code for the Post component which imports from DeletePost:
class PostsList extends Component {
renderData(){
return this.props.posts.map(post => {
const {title, social, link, link_image, time=moment(post.createdAt).fromNow()} = post;
return (
<div key={post._id} className='social-post'>
<img src={link_image}></img>
<p>{social}, {time}</p>
<a className='social-link' target="_blank" href={link}>{title}</a>
<div className='list-buttons'>
<button className='form-button button-gradient'>Edit</button>
<button type="button" className='form-button button-gradient' data-toggle="modal" data-target="#modalDelete">Delete</button>
</div>
<DeletePost />
</div>
);
})
}
render() {
return (
<div className='flex-timeline'>
{this.renderData()}
</div>
);
}
}
You have to pass the post value from its parents to child:
Your PostsList Class need to pass post value to child.
class PostsList extends Component {
renderData(){
return this.props.posts.map(post => {
const {title, social, link, link_image, time=moment(post.createdAt).fromNow()} = post;
return (
<div key={post._id} className='social-post'>
<img src={link_image}></img>
<p>{social}, {time}</p>
<a className='social-link' target="_blank" href={link}>{title}</a>
<div className='list-buttons'>
<button className='form-button button-gradient'>Edit</button>
<button type="button" className='form-button button-gradient' data-toggle="modal" data-target="#modalDelete">Delete</button>
</div>
<DeletePost post={post}/>
</div>
);
})
}
render() {
return (
<div className='flex-timeline'>
{this.renderData()}
</div>
);
}
}
Your DeletePost use this.props.post to access data from parent.
class DeletePost extends Component {
handleDelete(event) {
event.preventDefault();
Meteor.call('posts.remove', this.props.post);
$('#modalDelete').modal('hide');
}
render() {
return (
<div className="modal fade form-delete" id="modalDelete" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div className="form-outer">
<form id='delete_post' onSubmit={this.handleDelete.bind(this)}>
<div className='form-text form-header'>
<p>My dear, <strong>master</strong></p>
<p>Are you really sure about that?</p>
</div>
<button type="button" className="form-button button-delete" data-dismiss="modal">No</button>
<button type="sumbit" className="form-button button-delete">Yes</button>
</form>
</div>
</div>
);
}
}
If my api takes the query as http://localhost:8000/api/v1/rental/?place__startswith=kathmandu then how can i do generic search in reactjs. What i tried is i passed the default parameter as search(query=kathmandu) so that the result of place named kathmandu will be listed by default and when user types place name that they want to search then it should display those places instead of kathmndu. But i am getting an error saying Uncaught ReferenceError: kathmandu is not defined. How can i resolve this problem?
componentWillMount(){
this.search();
}
search(query=kathmandu){
let url = 'http://localhost:8000/api/v1/rental/?place__startswith=query';
Request.get(url).then((response) => {
console.log('response',response.body.objects);
this.setState({
place:response.body.objects
});
});
}
searchUpdated(term){
console.log('term is',term);
this.search(term);
}
render() {
var margin = { marginTop : '13em' };
let location = _.map(this.state.place, (place) => {
return(
<div className="searchResult">
<li>{place.place}</li>
<li>{place.city}</li>
</div>
)
});
return(
<div className = "container">
<div className="content text-align-center">
<div className="row text-xs-center">
<div className="middle-text" style={margin}>
<h1 className="welcome"><span>Welcome </span></h1>
<button ref="test" className="btn how-it-works" onClick={this.handleClick}>Search Space</button>
</div>
</div>
</div>
<div id="mySearch" className="overlay" onKeyDown={this.handleKeyDown}>
<button className="btn closebtn" onClick={this.handleClick}>x</button>
<div className="overlay-content">
<SearchInput ref="searchInput" className="search-input" onChange={this.searchUpdated} />
<ul>{location}</ul>
</div>
</div>
</div>
);
}
}
I think what you're looking for is encodeURIComponent.
search( query='kathmandu' ){
And:
let url = 'http://localhost:8000/api/v1/rental/?place__startswith=' + encodeURIComponent(query);
NB as your query string actually does only contains letter, you don't need encodeURIComponent for that example, but you might need it in other cases.