I want to update the todoList in my PARENT COMPONENT after I have added a new item in my child using the AddItem() method. Nothing gets added the first time.
EX. if I add "take test" doesn't get render, then if I add "take shower" doesn't get rendered but now "take test" does. Then if I add "take a leak" "take shower" gets rendered.
PARENT COMPONENT
firstUpdated(changedProperties) {
this.addEventListener('addItem', e => {
this.todoList = e.detail.todoList;
});
}
render() {
return html`
<p>Todo App</p>
<add-item></add-item>//Child item that triggers the add
<list-items todoList=${JSON.stringify(this.todoList)}></list-items>
`;
}
CHILD COMPONENT
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: storedLocalList }
})
);
this.todoItem = '';
}
}
render() {
return html`
<div>
<input .value=${this.todoItem} #keyup=${this.inputKeyup} />
<button #click="${this.AddItem}">Add Item</button>
</div>
`;
}
You need to set properties for todoItem
static get properties() {
return {
todoItem: {
type: Array,
Observer: '_itemsUpdated'
}
}
constructor(){
this.todoItem = []
}
_itemsUpdated(newValue,oldValue){
if(newValue){
-- write your code here.. no event listeners required
}
}
In above code., We need to initialise empty array in constructor.
Observer observe the changes to array & triggers itemsUpdated function which carries oldValue & NewValue. In that function., you can place your logic.
No Event Listeners required as per my assumption
Found my error. I was passing to detail: { todoList : storedLocalList } which is the old array without the updated value.
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: todoList }
})
);
this.todoItem = '';
}
}
Related
I'm struggling while creating an element that is passed by the .map function. Basically, I want my webpage to create a div element with some date in it when a button is clicked for that I'm using a .map function but it isn't working out.
const handleSubmit = (e) => {
e.preventDefault();
const data = {title:`${title}`, desc:`${desc}`, date:`${date}`};
data.map(userinfo =>{
return(<div>
<h1>{userinfo.title}</h1>
</div>)
})
console.log(data);
}
In reactJS, if we want to display our data in HTML webpage we usually do that in the render funciton.
We can use userInfo variable in the state object.
The userInfo data is hardcoded for demonstration purposes but you can also populate the userInfo variable either using API or in any other way you like.
Moreover, showUserInfo is another variable (initially false) that would render the data once it is set to true
this.state = {
userInfo: [
{
title: 'one',
desc: '',
date: new Date()
},
{
title: 'two',
desc: '',
date: new Date()
}
],
showUserInfo: false
}
On a click event we can set showUserInfo to true using setState function.
more on setState function via this link ->
https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b
handleSubmit = async (event) => {
event.preventDefault();
this.setState(
{
...this.state,
showUserInfo: true
}
)
}
In the render function, if showUserInfo is false then userInfo.map is never going to render unless showUserInfo is set to true which we do using a click listener that is associated with our function handleSubmit.
render(){
return (
<div>
<button onClick={this.handleSubmit}>Click Me</button>
{ this.state.showUserInfo &&
this.state.userInfo.map(item =>(
<div>
<p> {item.date.toString()} </p>
</div>
) ) }
</div>
);
}
Overall the result looks a something like this.
export default class App extends React.Component {
constructor() {
super();
this.state = {
showUserInfo: false,
userInfo: [
{
title: 'one',
desc: '',
date: new Date()
},
{
title: 'two',
desc: '',
date: new Date()
}
],
}
}
handleSubmit = async (event) => {
event.preventDefault();
this.setState(
{
...this.state,
showUserInfo: true
}
)
}
render(){
return (
<div>
<button onClick={this.handleSubmit}>Click Me</button>
{ this.state.showUserInfo &&
this.state.userInfo.map(item =>(
<div>
<p> {item.date.toString()} </p>
</div>
) ) }
</div>
);
}
}
I'm trying to update the styling of a <div> tag of my component based when data gets updated in the parent component. More specifically, it's a list of <div>: when one element of this list is onHover, I try to trigger other images to visibility: "hidden". Here's what I have so far (I've commented so that it helps in clarity):
componentDidUpdate(nextProps, prevState) {
// if a componnent has been hovevered
if (nextProps.isSelectedMouseEnterThumbnail) {
// trigger the function to updated the div located within this component
return this.updateOnHoverImage(nextProps, prevState)
} else {
return null;
}
};
updateOnHoverImage = (nextProps, prevState) => {
let componentIndex = this.props.index;
let onHoverComponentIndex = nextProps.selectedMouseEnterIndex;
// for each component, it displays:
// componentIndex: its index
// onHoverComponentIndex: the index of the hovered component;
// it does not start with null or undefined;
console.log(componentIndex, onHoverComponentIndex);
// if both matches, then trigger {visibility: "visible"}
if (onHoverComponentIndex === componentIndex) {
console.log(`visible for: ${componentIndex}`);
return {
visibility: "visible"
}
// Otherwise, trigger {visibility: "hidden"}
} else if (onHoverComponentIndex !== componentIndex) {
console.log(`hidden for: ${componentIndex}`);
return {
visibility: "hidden"
}
} else {
return null;
}
};
Then, in my parent component:
constructor(props) {
super(props);
this.state = {
counter: 0,
selectedSection: "all",
mock_data: mock_data,
dataOnView: null,
sections: ["all", "film", "music", "commercial"],
selectedMouseEnterIndex: null,
isSelectedMouseEnterThumbnail: false
};
};
<IndexImageComponent
onSelectedMouseEnterIndexImage={this.onSelectedMouseEnterIndexImage}
onSelectedMouseLeaveIndexImage={this.onSelectedMouseLeaveIndexImage}
index={index}
ele={ele}
{...this.state}
/>
onSelectedMouseEnterIndexImage = (index) => {
this.setState({
selectedMouseEnterIndex: index,
isSelectedMouseEnterThumbnail: true
})
};
onSelectedMouseLeaveIndexImage = (index) => {
this.setState({
selectedMouseEnterIndex: null,
isSelectedMouseEnterThumbnail: false
})
}
When I hover, the console.log(visible for: ${componentIndex}); and console.log(hidden for: ${componentIndex}); do work. But when I implement the style, such as this:
<div
style={this.updateOnHoverImage()}
onMouseEnter={() => this.onSelectedMouseEnterIndexImage(index)}
onMouseLeave={() => this.onSelectedMouseLeaveIndexImage(null)}
className="index_image_component">
this errror happens: TypeError: Cannot read property 'selectedMouseEnterIndex' of undefined.
I'm having trouble updating a list of elements using React, when I run the code below and click on a 'star' element, react updates ALL the elements in this.state.stars instead of just the element at the given index:
class Ratings extends React.Component {
constructor(props) {
super(props);
let starArr = new Array(parseInt(this.props.numStars, 10)).fill({
icon: "*",
selected: false
});
this.state = {
stars: starArr
};
this.selectStar = this.selectStar.bind(this);
}
selectStar(ind) {
this.setState({
stars: this.state.stars.map((star, index) => {
if (index === ind) star.selected = !star.selected;
return star;
})
});
}
makeStars() {
return this.state.stars.map((star, ind) => (
<span
className={star.selected ? "star selected" : "star"}
onClick={() => this.selectStar(ind)}
key={ind}
>
{star.icon}
</span>
));
}
render() {
return (
<div className="star-container">
<span>{this.makeStars()}</span>
</div>
);
}
}
Can anyone point me in the right direction here? Not sure why this is happening!
Your problem is in how you're instantiating your Array:
let starArr = new Array(parseInt(this.props.numStars, 10)).fill({
icon: "*",
selected: false
});
What that line is doing is filling each item in the array with the same object. Not objects all with the same values, but a reference to the same object. Then, since you're mutating that object in your click handler, each item in the Array changes because they're all a reference to that same object.
It's better to do a non-mutating update, like this:
this.setState({
stars: this.state.stars.map((star, index) =>
(index === ind)
? { ...star, selected: !star.selected }
: star
)
});
This will instead create a copy of the object at the Array index except with the selected property toggled.
Hi guys Im working with Vue, and Im trying to make recursive Tree from Flat list, I would like to toogle expanded property of each item when someone clicks on it, but for some reason it is not changin
It is happening in this function
expandNode(item) {
console.log("HERERERER");
item.expand = false;
this.$set(item, "expand", false);
}
I would like my Array to be reactive but for some reason it is not, Is it maybe the way Im reapacking the data or something else, Could someone take a look ??
Here is my CodeSandbox
https://codesandbox.io/s/condescending-tree-51rbs
this is the component code
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<tr v-for="(item ,index) in flatArray" :key="index">
<div class="item" #click="expandNode(item)">
<div class="element" v-show="item.expand">
{{ item.expand }}
<span>{{ item.label }}</span>
</div>
</div>
</tr>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
data: { default: () => null, type: Array }
},
data() {
return {
flatArray: []
};
},
mounted() {
let arr = [];
console.log("HERERER");
this.recursive(this.data, arr, 0, null, -1);
this.flatArray = arr;
console.log(this.flatArray);
},
computed: {
setPadding(item) {
return `padding-left: ${item.level * 30}px;`;
}
},
methods: {
recursive(obj, newObj, level, parent, parentIndex) {
obj.forEach(node => {
if (node.children && node.children.length != 0) {
node.level = level;
node.leaf = false;
node.expand = true;
node.parent = parent;
node.parentIndex = parent ? parentIndex : null;
newObj.push(node);
this.recursive(
node.children,
newObj,
node.level + 1,
node,
newObj.indexOf(node)
);
} else {
node.level = level;
node.leaf = true;
node.expand = true;
node.parent = obj;
node.parentIndex = parent ? parentIndex : null;
newObj.push(node);
return false;
}
});
},
expandNode(item) {
console.log("HERERERER");
item.expand = false;
this.$set(item, "expand", false);
}
}
};
</script>
the reason you're not seeing an update is that the array has no reason to recalculate. You're updating this.$set(item, "expand", false); an object that is not reactive. The reason it's not reactive is because you're not using $set method when you're creating the object.
here is what it would look like if you use $set correctly during object creation.
recursive(obj, newObj, level, parent, parentIndex) {
obj.forEach(node => {
this.$set(node, "level", level);
this.$set(node, "expand", true);
this.$set(node, "parentIndex", parent ? parentIndex : null);
if (node.children && node.children.length !== 0) {
this.$set(node, "leaf", false);
this.$set(node, "parent", parent);
newObj.push(node);
this.recursive(
node.children,
newObj,
node.level + 1,
node,
newObj.indexOf(node)
);
} else {
this.$set(node, "leaf", true);
this.$set(node, "parent", obj);
newObj.push(node);
return false;
}
});
},
note that you can now use item.expand = false
expandNode(item) {
item.expand = false;
// this.$set(item, "expand", false); <== not needed
}
you can see in action here
Alternatively,...
Here is code that might work for you that doesn't rely on reactivity
Note that:
I'm re-calculating and re-assigning the array with this.flatArray = flattenTree(this.data);
The idea is that the nested objects are a "source of truth", and the flattened array is there to allow the template to render.
<template>
<div class="hello">
<tr v-for="(item ,index) in flatArray" :key="index">
<div
#click="toggleExpandNode(item)"
class="item"
:style="{'margin-left':item.level * 1.6 +'em'}"
>
<div class="element">
<span v-if="item.leaf">⚬</span>
<span v-else-if="item.expand">▾</span>
<span v-else>▸</span>
<span>{{ item.label }}</span>
</div>
</div>
</tr>
</div>
</template>
<script>
const flattenTree = obj => {
const flatTreeArr = [];
let depth = 0;
const flatten = (node, parentNode) => {
flatTreeArr.push(node);
node.level = depth;
node.leaf = true;
node.parent = parentNode;
node.expand = node.expand === undefined ? true : node.expand;
if (node.children) {
node.leaf = false;
if (node.expand) {
depth++;
node.children.forEach(br => flatten(br, node));
depth--;
}
}
};
obj.forEach(br => flatten(br, null));
return flatTreeArr;
};
export default {
props: {
data: { default: () => null, type: Array }
},
data() {
return {
flatArray: []
};
},
mounted() {
this.flatArray = flattenTree(this.data);
},
methods: {
toggleExpandNode(item) {
item.expand = !item.expand;
this.flatArray = flattenTree(this.data);
}
}
};
</script>
see it in action here
If item object has no initial expand property, you must declare it as observable using this.$set(item, ...).
Directly adding new property like item.expand = ... skips this step and this.$set ignores it. Such property will not be reactive.
More about this in here: https://v2.vuejs.org/v2/guide/reactivity.html
I'm working on a component that should be able to:
Search by input - Using the input field a function will be called after the onBlur event got triggered. After the onBlur event the startSearch() method will run.
Filter by a selected genre - From an other component the user can select a genre from a list with genres. After the onClick event the startFilter() method will run.
GOOD NEWS:
I got the 2 functions above working.
BAD NEWS:
The above 2 functions don't work correct. Please see the code underneath. The 2 calls underneath work, but only if I comment one of the 2 out. I tried to tweak the startSearch() method in various ways, but I just keep walking to a big fat wall.
//////Searching works
//////this.filter(this.state.searchInput);
//Filtering works
this.startFilter(this.state.searchInput);
QUESTION
How can I get the filter/search method working?. Unfortunately simply putting them in an if/else is not the solution (see comments in the code).
import { Component } from 'preact';
import listData from '../../assets/data.json';
import { Link } from 'preact-router/match';
import style from './style';
export default class List extends Component {
state = {
selectedStreamUrl: "",
searchInput: "",
showDeleteButton: false,
searchByGenre: false,
list: [],
}
startFilter(input, filterByGenre) {
this.setState({
searchByGenre: true,
searchInput: input,
showDeleteButton: true
});
alert("startFilter ")
console.log(this.state.searchByGenre)
/////////---------------------------------
document.getElementById("searchField").disabled = false;
document.getElementById('searchField').value = input
document.getElementById('searchField').focus()
// document.getElementById('searchField').blur()
document.getElementById("searchField").disabled = true;
console.log(input)
this.filter(input);
}
//search
startSearch(input) {
alert("startSearch ")
console.log(this.state.searchByGenre)
//komt uit render()
if (!this.state.searchByGenre) {
//check for input
this.setState({
searchInput: input.target.value,
showDeleteButton: true,
})
//Searching works
//this.filter(this.state.searchInput);
//Filtering works
this.startFilter(this.state.searchInput);
// DOESNT WORK:
// if (this.state.searchInput != "") {
// this.filter(this.state.searchInput);
// } else {
// this.startFilter(this.state.searchInput);
// }
}
}
setAllLists(allLists) {
console.log("setAllLists")
console.log(this.state.searchByGenre)
this.setState({ list: allLists })
//document.body.style.backgroundColor = "red";
}
filter(input) {
let corresondingGenre = [];
let filteredLists = listData.filter(
(item1) => {
var test;
if (this.state.searchByGenre) {
alert("--this.state.searchByGenre")
//filterByGenre
//& item1.properties.genre == input
for (var i = 0; i < item1.properties.genre.length; i++) {
if (item1.properties.genre[i].includes(input)) {
corresondingGenre.push(item1);
test = item1.properties.genre[i].indexOf(input) !== -1;
return test;
}
this.setState({ list: corresondingGenre })
}
} else {
//searchByTitle
alert("--default")
test = item1.title.indexOf(input.charAt(0).toUpperCase()) !== -1;
}
return test;
})
console.log("filterdLists:")
console.log(filteredLists)
console.log("corresondingGenre:")
console.log(corresondingGenre)
//alert(JSON.stringify(filteredLists))
this.setState({ list: filteredLists })
}
removeInput() {
console.log("removeInput ")
console.log(this.state.searchByGenre)
this.setState({ searchInput: "", showDeleteButton: false, searchByGenre: false })
document.getElementById("searchField").disabled = false;
this.filter(this.state.searchInput)
}
render() {
//alle 's komen in deze array, zodat ze gefilterd kunnen worden OBV title.
if (this.state.list === undefined || this.state.list.length == 0 && this.state.searchInput == "") {
//init list
console.log("render ")
console.log(this.state.searchByGenre)
this.filter(this.state.searchInput)
}
return (
<div class={style.list_container}>
<input class={style.searchBar} type="text" id="searchField" placeholder={this.state.searchInput} onBlur={this.startSearch.bind(this)} ></input>
{
this.state.searchByGenre ?
<h1>ja</h1>
:
<h1>nee</h1>
}
{
this.state.showDeleteButton ?
<button class={style.deleteButton} onClick={() => this.removeInput()}>Remove</button>
: null
}
{
this.state.list.map((item, index) => {
return <div>
<p>{item.title}</p>
</div>
})
}
</div>
);
}
}
SetState is an async operation that takes a callback function. I suspect that your second function runs before the first SetState is finished.
Also, you are modifying the DOM yourself. You need to let React do that for you just by modifying state. I don't have time to write up an example now, but hopefully this helps in the meantime.
can you modify your search func,
//search
startSearch(input) {
const { value } = input.target
const { searchInput } = this.state
if (!this.state.searchByGenre) {
this.setState(prevState => ({
searchInput: prevState.searchInput = value,
showDeleteButton: prevState.showDeleteButton = true,
}))
JSON.stringify(value) !== '' ? this.filter(value) : this.startFilter(searchInput)
}
}