Use (nested) prop value to reference another prop - reactjs

I'm attempting to consume a nested prop value, then use that value to dynamically fetch another prop. This works for shallow (first level) props, but it fails when the prop is nested.
function DynamicContent(props) {
const content = props.data[props.children]
return <span>{content}</span>
}
Works (returns "My Post Title):
{
children: ["postTitle"],
data: {
postTitle: "My Post Title",
category: {
title: "The Category Title",
}
}
}
Does NOT work (returns undefined, expect "The Category Title"):
{
children: ["category.title"],
data: {
postTitle: "My Post Title",
category: {
title: "The Category Title",
}
}
}

You are doing great, you have done right. You need just a simple tweak.
Using eval you can evaluate as much nested as you want.
const path = 'props.data' + props.children;
const content = eval(path);

You cannot do this, but a simple solution is to make something like this :
children: ["category", "title"]
And inside your function :
const content = props.data[props.children[0]][props.children[1]]
I assume, is not better solution but you have an idea for achieve what you want, maybe you can split into two functions one for access property inside object, and the second for access to sub child into object

Related

TypeScript - How to infer a type from array of objects

I am building a React form component with TypeScript and want to type the data returned by my form.
My component accepts a prop called "fields" with the following structure:
const fields = [
{
name: "title",
default: "",
data: undefined,
size: 2
},
{
name: "users",
default: [],
data: [{id: 1, ...}]
size: 8
},
]
I would like to find a way to retrieve the type of the data returned by component based on the "field" variable. So, basically, I want to get something like that:
type FormData = {
title: string;
users: Array<{id: number, ...}>
}
The best solution would be to infer a different type depending on the "data" key. If "data" exists, then the field will have the same type as data, otherwise it is the type of the "default" key. All other keys should be ignored.
I believe I should be using generics to achieve this but I am not even sure this is something possible with TS and I have to admit that I can't figure it out...
Has anyone already faced this situation and found a solution to a similar issue ?
Thank you very much!
Based on the given array fields, we can create the type FormData with the following generic type as long as the fields variable was initialized with as const to stop TypeScript from widening the string literals.
const fields = [
{
name: "title",
default: "",
data: undefined,
size: 2
},
{
name: "users",
default: [],
data: [{id: 1}],
size: 8
},
] as const
type FormData<T extends readonly { name: st
[E in T[number] as E["name"]]: E["data"]
? E["default"]
: E["data"]
}
type Result = FormData<typeof fields>
// type Result = {
// title: "";
// users: readonly [{
// readonly id: 1;
// }];
// }
This might or might not work in the context of your component. But you did not show us the component itself. This is a solution based on the information given in your question.
Playground

Array to excel React

I am a little new to this, I am trying to pass my data array to an excel, but nothing works for me so far
I have an example
import {ExcelFile, ExcelSheet} from "react-export-excel";
const [listSelectNewTable, setListSelectNewTable] = useState([])
const DataExcel = [
{
columns: [
{ value: "Clave Cliente", widthPx: 50 },
{ value: "Nobre Cliente", widthCh: 20, widthCh: 20 },
{ value: "Clave Articulo", widthPx: 60},
{ value: "Nombre Cliente", widthPx: 60},
{ value: "Clave Unidad", widthPx: 60},
{ value: "Precio", widthPx: 60},
],
data: [listSelectNewTable],
}
];
class Download extends React.Component {
render() {
return (
<ExcelFile>
<ExcelSheet dataSet={DataExcel} name="Price"/>
</ExcelFile>
);
}
Can someone tell me what I'm doing wrong?
Before we begin:
Well obviously you're not going to have any data, if you're not passing any to the dataSet:
const [listSelectNewTable, setListSelectNewTable] = useState([])
// listSelectNewTable = []
Also you are attempting to call useState outside of a reactjs component and then proceed to use a class component later down the line. Without meaning to sound condescending, you really should study up on when to use life-cycle methods and when to use hooks (hint: it's either one or the other), because it's outside of the scope of this question / answer, but your code will never work if you don't put the time in to at least understand the basics.
The remainder of my answer assumes you at least fixed that.
The Problem:
Furthermore, even if it had any data, it would result in an error, because your data is incorrectly typed:
The dataSet prop type is defined as:
dataSet: Array<ExcelSheetData>
considering the following package type definitions:
interface ExcelSheetData {
xSteps?: number;
ySteps?: number;
columns: Array<string>;
data: Array<ExcelCellData>;
}
type ExcelCellData = ExcelValue | ExcelCell;
type ExcelValue = string | number | Date | boolean;
interface ExcelCell {
value: ExcelCell;
// [Note]: this is actually incorrectly typed in the package itself.
// Should be value: ExcelCellValue instead
style: ExcelStyle;
}
Essentially, if you had typescript enabled (which by the way I'd advise you to do), you'd see you're attempting to pass incorrect data-type to the dataSet property.
What you should be passing to dataSet prop is
Array<ExcellCellData>
// expecting: Array<ExcelValue | ExcelCell> (eg. ['A', 22])
What you are actually passing to to dataSet prop is
Array<Array<never>>
// received: [[]]
// [] = Array<never>
As you can see, the Array<Array<>> wraps it twice, meaning you are passing an unnecessarily wrapped array there.
Solution:
Now that we know the cause, let's fix it:
const DataExcel = [
{
columns: [
{ value: "Clave Cliente", widthPx: 50 },
{ value: "Nobre Cliente", widthCh: 20, widthCh: 20 },
{ value: "Clave Articulo", widthPx: 60},
{ value: "Nombre Cliente", widthPx: 60},
{ value: "Clave Unidad", widthPx: 60},
{ value: "Precio", widthPx: 60},
],
data: listSelectNewTable, // previously was [listSelectNewTable]
// Note we removed the extra wrapping nested array [] ˆ
},
];
Now you are passing your data correctly - but obviously you're not going to have anything inside the excel sheet barring the columns unless you pass any actual data instead of the empty array.
Final note (not necessary for answer):
Also in general I'd recommend using packages that have higher star ratings on their github page (this one has 65) and is only a fork of a an already existing package, especially for more complex stuff like excel exporting. While there sometimes truly are hidden diamonds in the rough, most of the time the stars are a good indicator of the quality of the package.
The fact that I found a typing error inside my own answer after a random glance at the package for the first time makes me fear how many other errors are actually in it, unless you know and trust the author or it's a package for a very niche thing.
Because the recursive
interface ExcelCell {
value: ExcelCell; // should be ExcelCellValue
style: ExcelStyle;
}
makes positively no sense and would just require you to recursively state the object interface definition ad infinitum
There are a couple of things that you need to change:
You cannot use the useState hook with a class component. To use this hook, change your component to a functional one.
You are not using the library correctly. Here is a good example of how to use it: https://github.com/rdcalle/react-export-excel/blob/master/examples/simple_excel_export_01.md
Specifically, you did not add any column to the sheet.
I'm guessing you want to provide dynamic data for the component (it should not always display the same stuff). If you want to do this, you should inject the data at the parent's component level.
So for your class:
import React, { useState } from 'react';
import ReactExport from "react-export-excel";
const ExcelFile = ReactExport.ExcelFile;
const ExcelSheet = ReactExport.ExcelFile.ExcelSheet;
const ExcelColumn = ReactExport.ExcelFile.ExcelColumn;
const Download = ({ data }) => {
const [listSelectNewTable, setListSelectNewTable] = useState(data);
return (
<ExcelFile>
<ExcelSheet data={listSelectNewTable} name="Price">
{
listSelectNewTable[0].columns.map(d =>
<ExcelColumn label={d.value} value={0} />)
}
</ExcelSheet>
</ExcelFile>
); }
export default Download;
And for the parent:
<Download data={DataExcel} />
With the data you had originally in your class in the parent, so that you can inject it into your component.

How to reassign the nested array keys based on Angular Reactive Forms?

I am working to recreate the nested array for Angular Reactive Forms.
Existing Nested Array.
nestedArray = [{
id:'abc',
required:true,
children:[{
id:"bcd",
parentId:"abc",
required:true,
children:[{
id:"cde",
parentId:"bcd",
required:false,
children:[{
id:"def",
parentId:"cde",
required:true,
children:[{
id:"efg",
parentId:"def",
required:false,
}]
},
{
id: "xyz",
parentId: "cde",
required: true,
}]
}]
}]
}]
Recreate this array for Angular Reactive Forms
nestedArray= this.fb.group({
abc: [''],
bcd: this.fb.group({
cde: [''],
efg: [''],
}),
});
Above array is incorrect, looking for the right way to create the children in Angular Reactive Form.
Thanks
You need make a recursive function that return a FormControl or a FormGroup
form = new FormArray(this.nestedArray.map(x=>{
const group=new FormGroup({})
group.addControl(x.id,this.getGroup(x))
return group
}));
getGroup(data: any) {
if (!data.children)
return new FormControl()
const group = new FormGroup({});
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x))
})
}
return group;
}
See stackblitz
Update really the answer is wrong, becaouse we has no form control to the "parents". the function must be like
form = new FormArray(this.nestedArray.map(x=>this.getGroup(x)))
getGroup(data: any) {
if (!data.children)
{
return new FormControl)
}
const group = new FormGroup({});
group.addControl(data.id,new FormControl())
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x))
})
}
return group;
}
Well, the question is how conrol this crazy form. We can try create a "recursive template", but I think it's better another aproach. Imagine you has an array of elements, each one with three porperties: "index","label" and "control". And control is a reference to the FormControl of our form. Then we can construct an .html like
<div *ngFor="let item of controls">
<span [style.margin-left]="item.index*10+'px'">{{item.label}}</span>
<input [formControl]="item.control">
<span *ngIf="item.control.errors?.required">*</span>
</div>
easy no? just use directly [formControl] and item.control.errors or item.control.touched
Well, for this first declare an empty array
controls:any[]=[];
And change the fuction so we add the group in two steps: 1.-create the formControl, 2.-Add to the group, the third step is add to our array an object with {label,control and "index"}. Well it's typical when we has a recursive function to want know the "index" of the recursion.
//we pass as index 0 at first
form = new FormArray(this.nestedArray.map(x=>this.getGroup(x,0)))
getGroup(data: any,index:number) {
if (!data.children)
{
//create the control
const control=new FormControl('',data.required?Validators.required:null)
//add to the array this.controls
this.controls.push({control:control,label:data.id,index:index})
//return control
return control
}
const group = new FormGroup({});
index++
//create the control
const control=new FormControl('',data.required?Validators.required:null)
//add to the array this.controls
this.controls.push({control:control,label:data.id,index:index})
//add the control to the group
group.addControl(data.id,control)
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x,index++))
})
}
return group;
}
Well, In the stackblitz add the property "value" to our complex object, to show how can do it, and we use as "label" the "id" -perhafs our complex object need a new property label-
just a pseudo code below, but you will need to iterate and create the final key-value object to get similar structure...
anyhow there are some tools solving similar issues, consider this: https://jsonforms.io/ - i'd consider those first.
function patch (array: any[]): any {
let res: any = {};
for (let a of array) {
res[a.id] = a;
if (a.children && a.children.length) res[a.id]['children'] = patch(a.children)
}
return res;
}

Can't access nested stateful objects in ReactJS

I am trying to access a nested object in ReactJS. This is what the object looks like:
const characteristics = [
{ id: "geo", content: 'Geopolitical' },
{ id: "dog", content: 'Dog Loving' },
];
const starterColumns = {
"1": {
name: 'I am',
items: characteristics
},
"2": {
name: 'fullstack developer',
items: []
}
}
const [columns, setColumns] = useState(starterColumns);
This is the error I get when I try to console.log(columns['2']['items']['0']['id']):
TypeError: Cannot read property 'id' of undefined
Does this have to do with the fact that I am working with a stateful variable? Is something funky going on with the nested objects? Thanks for the help!
EDIT
The problem was that there was no object in the column so I had no object to access. Now the problem outstanding is how do I fill that void without displaying a new drag and drop piece. Thanks for helping!
EDIT
I used a try/catch statement to check the object so if it is empty, nothing happens.
Use try catch only for errors that you can't handle
To access an element use dot notation whenever it's possible instead of using bracket notation []
When there is an empty array in the items you can't access to the id you will get an error so the solution is to check if the array is not empty
columns['2'].items.length > 0
To access the first element of an array you have to use [0] instead of ['0']
try this solution
if (columns['2'].items.length > 0) {
console.log(columns['2'].items[0].id)
}

Redux updating nested immutable data

I have an issue with updating the immutable redux and quite nested data. Here's an example of my data structure and what I want to change. If anyone could show me the pattern of accessing this update using ES6 and spread operator I would be thankful.
My whole state is an object with projects (key/value pairs - here as an example only one project) that are objects with its own key (and the keys are ids as well), arrays of procedures and inside the tasks:
{ 1503658959473:
{ projectName: "Golden Gate",
projectLocation": "San Francisco",
start:"22/09/1937",
id:1503658959473,
procedures:[
{ title: "Procedure No. 1",
tasks:[
{name: "task1", isDone: false},
{name: "task2", isDone: false},
{name: "task3", isDone: false}
]
}
]
}
}
What I'm willing to do is to update one single task 'isDone' property to 'true'. It's some kind of toggling the tasks. How can I return this state with that information updated?
The action creator pass this information to reducer:
export function toggleTask(activeProject, task, taskIndex) {
return {
type: TOGGLE_TASK,
payload: {
activeProject,
task,
taskIndex
}
};
}
You've run into a common issue with Redux. The docs recommend that you flatten your data structure to make it easier to work with, but if that's not what you want to do, I'd refer to this part of their docs.
Because both Object.assign() and the ...spread operator create shallow copies, you must go through each level of nest in your object and re-copy it.
Your code might look something like this...
function updateVeryNestedField(state, action) {
return {
...state,
procedures : {
...state.procedures,
tasks : {
return tasks.map((task, index) => {
if (index !== action.taskIndex) {
return task
}
return {
...task,
task.isDone: !task.isDone
}
}
}
}
}
}
I myself would create a new class called ProjectModel, which has a public method toggleTask that is able to update its task's status. The reducer state would be an object whose keys are project IDs and values are ProjectModel instances.

Resources