I have a angular 8 application. and I have two components. one component where you can edit a item. And one component where you can see all the items. The items are divided in four categories. And for each category there is a counter that counts the items in each category. If I add a item the counter increase, so that works correct. But if I delete a item. The counter doest'n decrease.
So I have this service:
_updateItemChanged = new BehaviorSubject<any>([]);
constructor() {}
}
and this is the
component where you can delete a item child component:
remove() {
this.dossierItemService.deleteDossierItem(this.data.dossierId, this.data.item.id)
.subscribe(() => {
this.dialogRef.close(true);
this.itemListService._updateItemChanged.next(this.data.item.title);
}, (error) => {
const processedErrors = this.errorProcessor.process(error);
this.globalErrors = processedErrors.getGlobalValidationErrors();
});
}
and this is the parent component.html:
<span i18n>Action steps</span>{{ dossierItemsCountString(itemTypes.ActionStep) }}
and this is parent.ts code
ngOnInit(): void {
this.itemlistService._updateItemChanged.subscribe(data => {
this.dossierItems = this.dossierItems.filter(d => d.title !== data);
});
}
and the dossierItemsCountString function:
dossierItemsCountBy(itemType: DossierItemTypeDto) {
return this.typeSearchMatches[itemType.toString()] || { total: 0, matches: 0 };
}
dossierItemsCountString(itemType: DossierItemTypeDto) {
const count = this.dossierItemsCountBy(itemType);
if (this.hasSearchQuery) {
return `(${count.matches}/${count.total})`;
} else {
return `(${count.total})`;
}
}
So what I have to change so that in the parent component also the counter decrease if you remove a item.
Thank you
After the discussion we found that the problem is in mixed pointers among components and the proper way to fix it is to manipulate the original array, instead of creating new one.
The solution:
this.itemlistService._updateItemRemoved.subscribe(data => {
const index = this.dossierItems.findIndex(a => a.id === data.id);
if (index !== -1) {
this.dossierItems.splice(index, 1);
}
});
Related
I'm writing a site where movies can be added to favorites, and then browse this list.
The card has a like. The add like function looks like this (where isAdd is a state variable)
MoviesCard.Js
function MoviesCard({film, onBookmarkClick, filmsSaved}) {
const [isAdded, setIsAdded] = useState(false);
function handleBookmarkClick() {
const newFavorite = !isAdded;
const savedFilmArr = filmsSaved.filter((obj) => {
return obj.movieId == film.id;
});
onBookmarkClick({...film, _id: savedFilmArr.length > 0 ? savedFilmArr[0]._id : null}, newFavorite);
}
const removeHandler = () => {
onBookmarkClick(film, false);
};
...other code
Next, through the props, I stretch on to the component one level higher
Movies.js
function Movies({openPopup, isLoading}) {
...
const [filmsSaved, setFilmsSaved] = useState([]);
...
async function onBookmarkClick(film,isAdded) {
if (isAdded) {
let jsonFilm = { //this data Json//};
try {
await mainApi.addMovies(jsonFilm).then((result) => {
setFilmsSaved([filmsSaved.push(result)]);
localStorage.setItem('filmsSaved', JSON.stringify(filmsSaved));
})
} catch {
openPopup(`add error`, false);
}
} else {
try {
await mainApi.deleteMovies(film._id);
setBookmarkClick(true);
setFilmsSaved([filmsSaved.splice(film._id)]);
localStorage.setItem('filmsSaved', JSON.stringify(filmsSaved))
} catch {
openPopup(`delete error`, false);
}
}
}
return (
<section>
<MoviesCardList
films={filmsSwitch ? films : filterShortFilm(films)}
onBookmarkClick={onBookmarkClick}
filmsSaved={filmsSaved}
/>
</section>
);
}
The request to add/remove a card to the database is working. But I want to use local storage, which will reduce the number of requests to the backend. But when you add a like to a card, the like is removed from the previously added card. And when deleting a like from a card, the likes are deleted from all the user's cards in the array of cards in the local stotage.
Where did I make a mistake?
JSON-Film-card
_id: ''
country:''
director:''
duration:''
year:''
description:''
trailerLink:''
thumbnail:''
owner:''
movieId:''
nameEN:''
__v:''
I am using state management in vue.js for fetching products. i am fetching products with ID from data base. i need to make two conditions for return if $state has already products.
first condition is if $state.products has any products than make it return, and
second condition if one ID is passed and related date according to ID is fetched, if again same ID is passed it will return value.
but i am confused how to set condition for multiple match. i also tried logical AND operator for this, but nothing work.
please help me to solve this.
Here is my code.
Product.vue
mounted () {
this.$store.dispatch("loadProducts",this.category);
},
computed:{
...mapState([
'products'
])
},
this is my file where i am handling state management for fetching products.
index.js
const store = new Vuex.Store({
state: {
pageTitle: '',
products: [],
},
mutations: {
setTitle(state, title) {
state.pageTitle = title;
}
},
actions: {
loadProducts({ commit }, id) {
if (this.state.products.length != 0) {
var i;
for (i = 0; i < this.state.products.length; i++) {
var j = this.state.products[i].category_id;
}
} else if (this.state.products.length != 0 && id == j) {
console.log("RETURN");
return;
} else {
axios.get('/loadProducts/' + id)
.then(({ data }) => {
this.state.products = data.items;
console.log(this.state.products);
commit('set_products', data.items)
})
}
},
},
mutations: {
set_products(state, products) {
state.products = products;
}
}
});
Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.
Below both code does exactly same but in different way. There is an onChange event listener on an input component. In first approach I am shallow cloning the items from state then doing changes over it and once changes are done I am updating the items with clonedItems with changed property.
In second approach I didn't cloned and simply did changes on state items and then updated the state accordingly. Since directly (without setState) changing property of state doesn't call updating lifecycles in react, I feel second way is better as I am saving some overhead on cloning.
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const clonedItems = Array.from(items);
clonedItems.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items: clonedItems });
};
OR
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items });
};
You can use this
this.setState(state => {
const list = state.list.map(item => item + 1);
return {
list,
};
});
if you need more info about using arrays on states, please read this: How to manage React State with Arrays
Modifying the input is generally a bad practice, however cloning in the first example is a bit of an overkill. You don't really need to clone the array to achieve immutability, how about something like that:
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const processedItems = items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
return {
...ele,
rate: Number(value)
};
} else {
return ele;
}
});
this.setState({ items: processedItems });
};
It can be refactored of course, I left it like this to better illustrate the idea. Which is, instead of cloning the items before mapping, or modifying its content, you can return a new object from the map's callback and assign the result to a new variable.
Using Angular, I have created a component that fires an initial web request along with two others used in loops. I am looping through these items to set siteCount which is declared in the first loop's request. I am using setTimeout() to push the totaled amount to an array, however this is probably not best practice.
constructor(public http: HttpClient, public chart: ChartDetails) {
this.http.get(`https://siteurl.com/items?$filter=Hub/Title eq 'Project Hub' and Active eq 'Yes'`).subscribe(data => {
data['value'].forEach(item => {
let siteCount: number = 0
this.http.get(`${item.Site}/lists/?$filter=BaseTemplate eq 171`).subscribe(data => {
data['value'].forEach(list => {
this.http.get(`${item.Site}/lists(guid'${list.Id}')/items`).subscribe(data => {
this.myTasks = data['value']
this.myTasks.forEach(variable => {
variable.internalListName = list.EntityTypeName.replace("List", "")
});
siteCount += data['value'].length
})
});
})
setTimeout(() => {
if (siteCount) {
this.chart.goalChartLabels.push(item.Title)
this.chart.goalChartData.push(siteCount)
}
}, 500);
});
})
}
Without using setTimeout, how can I push item.Title and siteCount when siteCount is done being totaled and before being reset to 0 for the next item?
My initial version would look like this:
import { zip } from 'rxjs';
...
this.http.get(`https://siteurl.com/items?$filter=Active eq 'Yes'`).subscribe(data => {
data['value'].forEach(item => {
this.http.get(`${item.Site}/lists/?$filter=BaseTemplate eq 171`).subscribe(data => {
const itemListObservables = data['value']
.map(list => this.http.get(`${item.Site}/lists(guid'${list.Id}')/items`));
zip(...itemListObservables)
.subscribe(itemLists => {
const totalCount = itemLists
.map(l => l.length)
.reduce((sum, val) => sum + val, 0)
if (totalCount > 0) {
this.chart.goalChartLabels.push(item.Title)
this.chart.goalChartData.push(totalCount)
}
itemLists.forEach(variable => {
variable.internalListName = list.EntityTypeName.replace("List", "");
this.myTasks.push(variable);
});
});
})
});
})
zip function waits for all observables to emit a value and returns an array of them.
As for best practices:
HttpService calls should be done at least in ngOnInit. You will probably use inputs for running these requests and they won't be defined in constructor.
Making three-level nested loops with HTTP requests on each level is not pretty. Consider making this a single resource on server side.
You should use forkJoin() to make all the requests and then get the response.
The rxjs docs link.