I'm not sure what I could do to make this work...So I'm calling API data (which is just a bunch of nested json objects) to create a menu. So far I have it working so that it renders the first layer of objects (first img), and when you click on each element, the next level shows up below it (second img). This works recursively, so I assume it to work for the next level down when I click on an element in "level 2", but this is not the case.
Can anyone see what I could change in my code to make this work?
class Menu extends React.Component {
state = {
categories: [],
list: ""
};
//handle displaying list of values when clicking on button
//search for list of values within object
handleSearch = (obj, next, event) => {
// console.log(event.target.name);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === "object") {
if (next === key) {
//create DOM CHILDREN
createData(Object.keys(obj[key]), key, this.test, event);
}
this.handleSearch(obj[key], next);
}
});
};
componentDidMount() {
axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
this.setState({ categories: response.data });
});
}
render() {
return (
<React.Fragment>
{/* display list of things */}
<div className="columns is-multiline">
{Object.keys(this.state.categories).map(key => (
<div className="column is-4">
<div
onClick={event =>
this.handleSearch(this.state.categories, key, event)
}
>
{key}
</div>
<div name={key + "1"} />
</div>
))}
</div>
</React.Fragment>
);
}
}
and here is the code for createData, which creates the next level of data and is supposed to make it so these elements can be clicked to show the next level
var index = 1;
export function createData(data, key, search, e) {
e.stopPropagation();
let parent = document.getElementsByName(key + "1");
//create elements and append them
for (let i = 0; i < data.length; i++) {
let wrapper = document.createElement("ul");
wrapper.innerHTML = data[i];
wrapper.name = data[i] + index + 1;
wrapper.addEventListener("click", search(data[i]));
parent[0].append(wrapper);
}
}
here's a screenshot of "categories" in the console (objects within objects within objects)
For simplicity we will render this nested object recursively which is structured as your categories object:
const categories = {
level1: {
"level1-1": {
"level1-1-1": {
"level1-1-1-1": {}
}
},
"level1-2": {
"level1-2-1": {}
}
},
level2: {}
};
Recursion end condition will be an object without keys.
For every layer, render the keys and make a recursion step.
const makeMenuLayer = layer => {
const layerKeys = Object.entries(layer).map(([key, value]) => (
<>
{key}
{makeMenuLayer(value)}
</>
));
return <div>{layerKeys}</div>;
};
Will result:
level1
level1-1
level1-1-1
level1-1-1-1
level1-2
level1-2-1
level2
Checkout the sandbox example.
You should try to avoid manipulating the DOM by creating/appending elements with vanilla JS while using React if possible, one of its main strengths is the virtual DOM which manages rendering for you and avoids conflicts.
Not sure how deep the objects you're rendering go, but due to the complexity of the data this would be a good case for using another component to modularize things and avoid the messiness you're dealing with now.
Assuming all the child objects are structured the same, you could create a component that renders itself recursively based on the object keys. This answer should help.
Related
I have an issue while rendering the following.
I have a useBoard hook, which should create a new board: a 2d Array, which every cell is an object, in which some of these object have a value between 1-3 included, and the other 'empty cells' have the 0 value.
Cell object:
{
value: 0,
className: ''
}
My useBoard simply call the function to create the board.
export const useBoard = ({ rows, columns, pieces }: IBoardItems) => {
const [board, setBoard] = useState<ICell[][]>();
useEffect(() => {
const getBoard = () => {
const newBoard = createBoard({ rows, columns, pieces });
setBoard(newBoard);
}
getBoard();
}, [])
return [board] as const;
};
And here is the utility function to build it.
export const createBoard = ({ rows, columns, pieces }: IBoardItems) => {
let randomIndexes: number[][] = [];
while (randomIndexes.length < pieces) {
let randomIndex = [getRandomNumber(5, rows), getRandomNumber(0, columns)];
if (randomIndexes.includes(randomIndex)) {
continue;
} else {
randomIndexes.push(randomIndex);
};
};
let board: ICell[][] = [];
for (let i = 0; i < rows; i++) {
board.push(Array(columns).fill(defaultCell));
};
randomIndexes.forEach(([y, x]) => {
board[y][x] = {
value: getRandomNumber(1, 3),
className: ''
}
});
return board;
};
The fact is, that when I click start in my related component, which should render the <Game /> containing the <Board />, sometimes it works, sometimes not, and the console logs the error 'Cannot set properties to undefined'. I think I understand but I'm not sure: is it because could happen, that in some steps, some data is not ready to work with other data?
So, in this such of cases, which would be the best approach? Callbacks? Promises? Async/await?
NB. Before, I splitted the createBoard function into smaller functions, each one with its owns work, but to understand the issue I tried to make only one big, but actually I would like to re-split it.
EDIT: I maybe found the issue. In the createBoard I used getRandomNumber which actualy goes over the array length. Now the problem no longer occurs, but anyway, my question are not answered.
Hi there SO!
I'm currently trying to make a form that generates based on the object supplied and this seem to work at just about anything I throw at it.
That is, until I get to a nested object.
The problem:
Once I hit the if condition (typeof value === "object") I want to have a hidden input (this works).
Then I want to go into that object I just identified, and into all child objects it may contain and generate the input on same criteria as the initial run-through.
function GenericForm(props: any) {
var object = props.object;
return (
<div>
<form>
{Object.entries(object).map(([property, value]) => {
let type: string = "";
if (typeof value === "string") {
type = "text";
} else if (typeof value === "number") {
type = "number";
} else if (typeof value === "boolean") {
type = "checkbox";
} else if (value instanceof Date) {
type = "date";
} else if (typeof value === "object") {
type = "hidden";
}
return [
<label property={property} htmlFor={property}>
{property}
</label>,
<input
type={type}
id={property}
name={property}
defaultValue={value as string}
onChange={(newVal) => {
object[property] = newVal.target.value;
}}
/>,
];
})}
</form>
</div>
);
}
export default GenericForm;
I'm aware that this most likely utilizes some kind of recursion and while I have tried to solve it using recursion, I haven't been able to solve it. the code pasted here is from before I tried recursion, to have a "clean sheet" from where it went wrong for me.
EDIT 1 - added info on object structure
the object passed should be completely generic and allow for objects of any structure to be handed to the component, currently it should then just evaluate what type the properties are and make an input element from that.
one of the current objects I'm passing have the following JSON Schema
{
"id": "XYZ1",
"type": "twilio-api",
"sid": "someSID",
"from": "+phonenumberhere",
"name": "TWILIO SMS",
"credentials": {
"token": "someapitoken"
}
}
above object currently renders like so:
Assuming you have a Input component:
function Input = ({ name, type, value }) => {
// most of your code can fit here
return <input name={name} type={type} value={value} />
}
You can use your version of code, I use a simplified version as above to make our discussion easier. With that we can design a InputList component:
function InputList = ({ object }) => {
console.log('list', object)
return (
<div>
{Object.entries(object).map(([property, value]) => {
if (typeof value === "object") {
return <InputList object={value} />
} else {
return <Input name={property} />
}
})}
</div>
)
}
You can see inside this InputList, there's a call to InputList again, so that is the recursion you are looking for. The recursion stops when you don't have an object inside an object any more.
NOTE: React requires value and onChange to drive any input box. Otherwise they'll just behave like a native input. But this is not part of this question.
have a simple React component that displayes a character and should call a handler when clicked, and supply a number. The component is called many times, thus displayed as a list. The funny thing is that when the handler is called, the supplied index is always the same, the last value of i+1. As if the reference of i was used, and not the value.
I know there is a javascript map function, but shouldn't this approach work too?
const charComp = (props) => {
return (
<div onClick={props.clicked}>
<p>{props.theChar}</p>
</div>
);
deleteHandler = (index) => {
alert(index);
}
render() {
var charList = []; // will later be included in the output
var txt = "some text";
for (var i=0; i< txt.length; i++)
{
var comp =
<CharComponent
theChar = {txt[i]}
clicked = {() => this.deleteHandler(i)}/>;
charList.push(comp);
}
Because by the time you click on a letter, i is already 9 and it will remain 9 since the information is not held anywhere.
If you want to keep track of the index you should pass it to the child component CharComponent and then pass it back to the father component when clicked.
const CharComponent = (props) => {
const clickHandler = () => {
props.clicked(props.index);
}
return (
<div onClick={clickHandler}>
<p>{props.theChar}</p>
</div>
);
};
var comp = (
<CharComponent theChar={txt[i]} index={i} clicked={(index) => deleteHandler(index)} />
);
A little codesandbox for ya
I made a sorting algorithm visualiser which displays vertical bars of different heights and sort them. I have used a button here called "Generate new Array" which will call a function to generate new array everytime and I have also used this function in componentDidMount() function. How do I change the style property whenever I click that button?
I tried taking document.getElementByClassName('array-bars') into an array and change its style property using loop but its not happeneing. I am adding the necessary code below.
{ //array is const storing array of numbers which is also only state of this program.
array.map((value, idx) => (
<div
className="array-bar"
key={idx}
style={{ height: value, backgroundColor: 'turquoise' }}></div>))
}
componentDidMount(){
this.resetArray();
}
// this is called when I click generate new array
resetArray(){
const array = [];
for (let i = 0; i < 300; i++) {
array.push(randomIntFromInterval(15, 650));
}
const arrayBars = document.getElementByClassName('array-bar');
for (let i = 0; i < arrayBars.length; i++)
arrayBars[i].style.backgroundColor = 'green'; //this is failing
this.setState({ array });
}
Edited:
This is function where I am changing style property using the method written in above code. Its working here.
Also, Can you tell how can I change the color in this mergeSort() in last?
I tried using this.setState() at last but that's changing the color in the beginning only.
mergeSort(){
for(let i=0;i<animations.length;i++){
const arrayBars= document.getElementsByClassName('array-bar');
const colorChange=i%3!==2;
if(colorChange){
const [barOne,barTwo] =animations[i];
const barOneStyle=arrayBars[barOne].style;
const barTwoStyle=arrayBars[barTwo].style;
const color=i%3===0?'red':'turquoise';
setTimeout(()=>{
barOneStyle.backgroundColor=color;
barTwoStyle.backgroudColor=color;
},i*2);
}
else{
setTimeout(()=>{
const[barOne,newHeight]=animations[i];
const barOneStyle=arrayBars[barOne].style;
barOneStyle.height=newHeight+'px';
},i*2)
}
}
}
In React, you should rely on state changes to "make things happen". As suggested by other in the question comments, set an initial state containing the initial background color and update the value as needed.
UPDATE: If you want changes to happen after button click, just set its onclick attribute to point to a function that does what you want. Here I added a button and pointed its onclick attribute to resetArrays.
class YourComponent extends React.Component {
constructor(props){
super(props);
this.state = {
barBg: 'turquoise'
}
}
render(){
return <div>
<button onClick={this.resetArray.bind(this)}>Generate new array</button>
{ //array is const storing array of numbers which is also only state of this program.
array.map((value, idx) => (
<div
className="array-bar"
key={idx}
style={{ height: value, backgroundColor: this.state.barBg }}></div>))
}
</div>
}
componentDidMount(){
this.resetArray();
}
// this is called when I click generate new array
resetArray(){
const array = [];
for (let i = 0; i < 300; i++) {
array.push(randomIntFromInterval(15, 650));
}
this.setState({ array, barBg: 'green' });
}
}
I'm creating a list in react native and each element is clickable. When the element is clicked it navigates to another Scene and passes an object based on the element that was clicked and the value of 'i'.
But when clicking an element it always send the object that 'i' ended on. Which makes sense.
constructor(props) {
super(props);
this.state = {
items: this.props.items
};
}
makeList() {
var items = []
for (var i = 0; i <3; i++) {
items.push(
<TouchableOpacity
activeOpacity={0.6}
onPress={() => this.sendItem(this.state.items[i])}>
<View>this.state.items[i].name</View>
</TouchableOpacity>
)
}
return items;
}
render() {
return (
<View>
{this.makeList()}
</View>
);
}
sendItem(item) {
this.props.navigate(item);
}
So whenever any one of the items are clicked it always send 'Four'.
How can i fix this so that it sends the correct object?
Thank you!
var doesn't create a new variable for each iteration of the loop, so you're mutating the same i variable. When the loop ends, i === 3, and so that's what the onPress callback sees. You can use let instead.
for (let i = 0; i <3; i++) {
Or you can avoid loops.
const items = Array.from({length: 3}, (x, i) => {
return <TouchableOpacity ... />;
});