I have an array of objects which I'm rendering by section - see title of each object "Price", "Sectors and Charges" etc.
This populates a mini modal where users can select options to update rendered columns basically a filter.
The selection of the items are working however if I make a selection of the first item "0" all sections with the first option are selected.
How can I store the selection from each object into the selectedOptions array?
Please note I'm using react js and styled components, I've not added the styled component code.
Data:
const columnsData = [
{
title: 'Price',
options: [
{
label: 'Daily Change'
},
{
label: 'Price'
},
{
label: 'Price Date'
},
{
label: 'Volatility Rating'
}
],
},
{
title: 'Sectors and Charges',
options: [
{
label: 'Sector'
},
{
label: 'Asset Class'
},
{
label: 'AMC'
},
],
},
{
title: 'Cumulative Performance',
options: [
{
label: '1 month'
},
{
label: '6 months'
},
{
label: '1 year'
},
],
},
]
Code:
const EditColumns = ({active, onClick}) => {
const [selectedOptions, setSelectedOptions] = useState([0, 1, 2]);
const update = () => {
onClick();
}
const updateSelection = (z) => {
setSelectedOptions(selectedOptions.includes(z) ? selectedOptions.filter(j => j !== z) : [...selectedOptions, z]);
}
return (
<Wrap onClick={() => update()}>
<CTA>
<SVG src="/assets/svgs/btns/edit.svg" />
<span>Columns</span>
</CTA>
{active &&
<Dropdown>
<Head>
<span className="title">Edit Columns</span>
<span>Select the columns you would like to see</span>
</Head>
<Body>
{columnsData.map((item, i) => {
return (
<Section key={i}>
<SectionHead>
<span className="title">{item.title}</span>
<span>Select all</span>
</SectionHead>
<SectionList>
{item.options.map((child, z) => {
const selected = selectedOptions.includes(z);
return (
<li key={z} className={classNames({selected})} onClick={() => updateSelection(z)}>
<span>{child.label}</span>
</li>
)
})}
</SectionList>
</Section>
)
})}
</Body>
</Dropdown>
}
</Wrap>
)
}
export default EditColumns;
Your section lists are all sharing the same state variable, so any changes will be applied to all of them. You could fix this either by constructing a more complex state object which more closely resembles the structure of columnsData, or making each SectionList its own component with its own state. What you decide to do will depend on the degree to which the EditButtons component actually needs access to the whole state.
The second approach might look something like this:
const EditColumns = ({active, onClick}) => {
const update = () => {
onClick();
}
return (
<Wrap onClick={() => update()}>
<CTA>
<SVG src="/assets/svgs/btns/edit.svg" />
<span>Columns</span>
</CTA>
{active &&
<Dropdown>
<Head>
<span className="title">Edit Columns</span>
<span>Select the columns you would like to see</span>
</Head>
<Body>
{columnsData.map((item, i) => {
return (
<Section key={i}>
<SectionHead>
<span className="title">{item.title}</span>
<span>Select all</span>
</SectionHead>
<SectionList options={item.options}/>
</Section>
)
})}
</Body>
</Dropdown>
}
</Wrap>
)
}
const SectionList = ({options}) => {
const [selectedOptions, setSelectedOptions] = useState([0, 1, 2]);
const updateSelection = (z) => {
setSelectedOptions(selectedOptions.includes(z) ? selectedOptions.filter(j => j !== z) : [...selectedOptions, z]);
}
return (
<SectionListContainer>
{options.map((child, z) => {
const selected = selectedOptions.includes(z);
return (
<li key={z} className={classNames({selected})} onClick={() => updateSelection(z)}>
<span>{child.label}</span>
</li>
)
})}
</SectionListContainer>
)
}
Related
I'm using React Hook Form to build a basic page builder application and it's been brilliant so far, I've been using the useFieldArray hook to create lists that contain items, however, I haven't found a way to move items between lists.
I know I can use the move() function to reorder items within the same list, however, since each list has its own nested useFieldArray I can't move the item from one list component to another list component.
If anyone knows of a way around this it would be much appreciated!
Here is a very simplified example of my current setup:
export const App = () => {
const methods = useForm({
defaultValues: {
lists: [
{
list_id: 1,
items: [
{
item_id: 1,
name: 'Apple'
},
{
item_id: 2,
name: 'Orange'
}
]
},
{
list_id: 2,
items: [
{
item_id: 3,
name: 'Banana'
},
{
item_id: 4,
name: 'Lemon'
}
]
}
]
}
});
return (
<FormProvider {...methods}>
<Page/>
</FormProvider>
)
}
export const Page = () => {
const { control } = useFormContext();
const { fields } = useFieldArray({
control,
name: 'lists'
})
return (
<ul>
{fields?.map((field, index) => (
<List listIdx={index} />
))}
</ul>
)
}
export const List = ({ listIdx }) => {
const { control, watch } = useFormContext();
const { fields, move } = useFieldArray({
control,
name: `lists[${sectionIdx}].items`
})
const handleMove = (prevIdx, nextIdx) => {
// this allows me to move within lists but not between them
move(prevIdx, nextIdx);
}
return (
<li>
<p>ID: {watch(lists[${listIdx}].list_id)}</p>
<ul>
{fields?.map((field, index) => (
<Item listIdx={index} itemIdx={index} handleMove={handleMove}/>
))}
</ul>
</li>
)
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
const { control, register } = useFormContext();
return (
<li>
<p>ID: {watch(lists[${listIdx}].items[${itemIdx}].item_id)}</p>
<label
Name:
<input { ...register('lists[${listIdx}].items[${itemIdx}]) }/>
/>
<button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
<button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
</div>
)
}
Thanks in advance!
If you'd not like to alter your default values (your data structure), I think the best way to handle this is using update method returning from useFieldArray. You have the data of both inputs that are going to be moved around, knowing their list index and item index, you could easily update their current positions with each other's data.
// while accessing the object values from data, I'm getting undefined in map
// ../data/section1
const data = [{
id: 1,
image: './images/homepage/xbox-games.png',
text: 'Buy Xbox games and consoles',
}, {
id: 2,
image: './images/homepage/shop_surface_devices.webp',
text: 'Shop surface devices',
}, {
id: 3,
image: './images/homepage/choose_your_ms_365.png',
text: 'Choose your Microsoft 365',
}, {
id: 4,
image: './images/homepage/shop_windows_10.png',
text: 'Shop Windows 10',
}]
export default data;
// the actual component
import data from "../data/section1";
const Section1 = () => {
return (
<>
<div class="mx-20">
{data.map((vals) => {
<div class="">
<img src={vals.image}/>
<p>{vals.text}</p>
</div>
})}
</div>
</>
)
}
export default Section1;
return JSX from the map
import data from "../data/section1";
const Section1 = () => {
return (
<>
<div class="mx-20">
{data.map((vals) => {
return (
<div class="">
<img src={vals.image}/>
<p>{vals.text}</p>
</div>
)
})}
</div>
</>
)
}
export default Section1;
I had the same problem, then tried the first bracket instead of the second, and it resolved the problem
import data from "../data/section1";
const Section1 = () => {
return (
<>
<div class="mx-20">
{data.map((vals) => (
<div class="">
<img src={vals.image}/>
<p>{vals.text}</p>
</div>
))}
</div>
</>
)
}
export default Section1;
I'm trying to add product items to my redux store called cart. After adding an item I then compare both stores product(redux store) and cart(redux store) to check if the product has the same itemCode(item code. if they do I would like to Hide the add button and show the remove button. Unfortunately I'm getting different results, please look at the picture below for reference:
interface IProps {
items: ItemInterface[];
documentItems: ItemInterface[];
onAddItem: any;
}
const ItemFlatList2: FC<Partial<IProps>> = ({
items,
documentItems,
onAddItem,
}) => {
return (
<div className={styles.container}>
<ul>
{items!.map((item) => {
return (
<div className={styles.itemContainer}>
<div key={item.itemCode}>
<li>{item.itemCode}</li>
<li>{item.itemDescription}</li>
{documentItems!.length === 0 ? (
<AddButton
title={"ADD"}
onClick={() =>
onAddItem(
item.itemCode,
item.itemDescription,
item.itemSellingPrice
)
}
/>
) : (
documentItems!.map((documentItem) => {
if (documentItem.itemCode === item.itemCode) {
return <RedButton title={"Remove"} />;
}
if (documentItem.itemCode !== item.itemCode) {
return (
<AddButton
title={"ADD"}
onClick={() =>
onAddItem(
item.itemCode,
item.itemDescription,
item.itemSellingPrice
)
}
/>
);
}
})
)}
</div>
<div>
<li>Hello world</li>
</div>
</div>
);
})}
</ul>
</div>
);
};
export default ItemFlatList2;
The Cart Store:
const initialState: Partial<DocumentDetailsInterface>[] = [
];
const cartStore = createSlice({
name: "quotation reducer",
initialState,
reducers: {
add: {
reducer: (
state,
{ payload }: PayloadAction<DocumentDetailsInterface>
) => {
state.push(payload);
},
prepare: (item) => ({
payload: item,
}),
},
edit: (state, { payload }) => {},
remove: (
state,
{ payload }: Partial<PayloadAction<DocumentDetailsInterface>>
) => {
const findItem = state.findIndex((item) => payload!.code === item.code);
if (findItem !== 1) {
state.splice(findItem, 1);
}
},
},
extraReducers: {},
});
and The Product Store:
const initialState: ItemInterface[] = [
{
_id: "sdfsd",
itemType: "Physical Item",
itemUnitOfMeasure: "Unit",
itemCode: "PPC10",
itemDescription: "PPC Cement",
itemCostPrice: 50,
itemSellingPrice: 80,
itemQuantity: 100,
vatStatus: "Standard rate 15%",
},
{
_id: "qew",
itemType: "Physical Item",
itemUnitOfMeasure: "Unit",
itemCode: "2",
itemDescription: "Sepako Cement",
itemCostPrice: 30,
itemSellingPrice: 60,
itemQuantity: 100,
vatStatus: "Standard rate 15%",
},
{
_id: "sdfsd",
itemType: "Physical Item",
itemUnitOfMeasure: "Unit",
itemCode: "1",
itemDescription: "PPC Cement",
itemCostPrice: 50,
itemSellingPrice: 80,
itemQuantity: 100,
vatStatus: "Standard rate 15%",
},
];
const itemSlice = createSlice({
name: "item reducer",
initialState,
reducers: {},
extraReducers: {},
});
It looks like you have wrong logic in you render method. You displayed "Add" button when there are no items in documentItems and the if any items inside you keep adding "Add" buttons if they are not equal to itemCode. So basically you have 2 loops. First is render items, and second one is to render buttons for each item. But you can use one loop to render items and have logic to check if that item is already in the documentItems array - if not then display "Add" button, else "Remove" button.
return (
<div className={styles.container}>
<ul>
{items!.map(item => {
return (
<div className={styles.itemContainer} key={item.itemCode}>
<div key={item.itemCode}>
<li>{item.itemCode}</li>
<li>{item.itemDescription}</li>
{documentItems!.findIndex(
documentItem => documentItem.itemCode === item.itemCode,
) === -1 ? (
<AddButton
title={'ADD'}
onClick={() =>
onAddItem(
item.itemCode,
item.itemDescription,
item.itemSellingPrice,
)
}
/>
) : (
<RedButton title={"Remove"} />
)}
</div>
<div>
<li>Hello world</li>
</div>
</div>
);
})}
</ul>
</div>
);
I think you need to provide a unique key to button. Write the statemen when you are changing the state
<AddButton
title={"ADD"}
key={item.itemCode}
onClick={() =>
onAddItem(
item.itemCode,
item.itemDescription,
item.itemSellingPrice
)
}
/>
You used map function to check if item exists in the documentItems. I think changing it to some function will work out.
documentItems!.some((documentItem) => {
return documentItem.itemCode === item.itemCode
}
) ? (<RedButton title={"Remove"} />): (
<AddButton
title={"ADD"}
onClick={() =>
onAddItem(
item.itemCode,
item.itemDescription,
item.itemSellingPrice
)
}
/>
);
I have an array of object with "id", "name", "value" that I pass to a component and it divided in row and col in this way:
export const RenderDetailRow = props => {
const columns = [];
props.content.forEach((content, idx) => {
columns.push(
<div className="col-sm py-3" key={`item_${idx}`}>
<b>{content.name + ': '}</b>
<Input type="text" name={content.name} id={content.id} readOnly value={content.value} />
</div>
);
if ((idx + 1) % props.display[0].number === 0) {
columns.push(<div className="w-100"></div>);
}
});
return (
<div className="row" style={{ margin: 30 }}>
{columns}
</div>
);
};
I have two kind of problem, the first:
Each child in a list should have a unique "key" prop.
I have inserted the key but I have this error.
If the number of field is odd I have a long Input, it is possible to create a empty field o something like this?
For example Date and Created By has every one 1/2 of the space, while Last Modified has 2/2. How can I do?
Thank you
EDIT.
props.display[0].number is only a number that i pass (for example 2,3,4) to obtain the number of cols
EDIT2:
Example of Array that I pass:
const Content = [
{
id: 'id',
name: 'ID',
value: realm.id,
},
{
id: 'realmId',
name: 'ID Realm',
value: realm.realmId,
},
{
id: 'name',
name: 'name',
value: realm.name,
}
]
const Display = [
{
number: 2,
},
];
so my render is:
render(
<RenderDetailRow content={Content} display={Display} />
)
For 1, you are missing key in if block
Try point 2:
function chunkArray(array, size) {
if(array.length <= size){
return [array]
}
return [array.slice(0,size), ...chunkArray(array.slice(size), size)]
}
export const RenderDetailRow = props => {
const columns = props.content.map((content, idx) => {
return (
<div
key={`item_${idx}`}
className="col-sm py-3"
>
<b>{content.name + ': '}</b>
<Input type="text" name={content.name} id={content.id} readOnly value={content.value} />
</div>
);
});
const rows = chunkArray(columns, NUMBER_OF_ROWS);
return rows.map((row, index) => (
<div className="row" key={index} style={{ margin: 30 }}>
{row[index]}
{row[index].length - NUMBER_OF_ROWS !== 0
? // (render row[index].length - NUMBER_OF_ROWS) empty columns here
: null}
</div>
))
};
I am trying to use JsonSchema-Form component but i ran into a problem while trying to create a form that, after choosing one of the options in the first dropdown a secondary dropdown should appear and give him the user a different set o options to choose depending on what he chose in the first dropdown trough an API call.
The thing is, after reading the documentation and some examples found here and here respectively i still don't know exactly how reference whatever i chose in the first option to affect the second dropdown. Here is an example of what i have right now:
Jsons information that are supposed to be shown in the first and second dropdowns trough api calls:
Groups: [
{id: 1,
name: Group1}
{id: 2,
name: Group2}
]
User: [User1.1,User1.2,User2.1,User2.2,User3.1,User3.2, ....]
If the user selects group one then i must use the following api call to get the user types, which gets me the the USER json.
Component That calls JSonChemaForm
render(){
return(
<JsonSchemaForm
schema={someSchema(GroupOptions)}
formData={this.state.formData}
onChange={{}}
uiSchema={someUiSchema()}
onError={() => {}}
showErrorList={false}
noHtml5Validate
liveValidate
>
)
}
SchemaFile content:
export const someSchema = GroupOptions => ({
type: 'object',
required: [
'groups', 'users',
],
properties: {
groups: {
title: 'Group',
enum: GroupOptions.map(i=> i.id),
enumNames: GroupOptions.map(n => n.name),
},
users: {
title: 'Type',
enum: [],
enumNames: [],
},
},
});
export const someUISchema = () => ({
groups: {
'ui:autofocus': true,
'ui:options': {
size: {
lg: 15,
},
},
},
types: {
'ui:options': {
size: {
lg: 15,
},
},
},
});
I am not really sure how to proceed with this and hwo to use the Onchange method to do what i want.
I find a solution for your problem.There is a similar demo that can solve it in react-jsonschema-form-layout.
1. define the LayoutField,this is part of the demo in react-jsonschema-form-layout.To make it easier for you,I post the code here.
Create the layoutField.js.:
import React from 'react'
import ObjectField from 'react-jsonschema-form/lib/components/fields/ObjectField'
import { retrieveSchema } from 'react-jsonschema-form/lib/utils'
import { Col } from 'react-bootstrap'
export default class GridField extends ObjectField {
state = { firstName: 'hasldf' }
render() {
const {
uiSchema,
errorSchema,
idSchema,
required,
disabled,
readonly,
onBlur,
formData
} = this.props
const { definitions, fields, formContext } = this.props.registry
const { SchemaField, TitleField, DescriptionField } = fields
const schema = retrieveSchema(this.props.schema, definitions)
const title = (schema.title === undefined) ? '' : schema.title
const layout = uiSchema['ui:layout']
return (
<fieldset>
{title ? <TitleField
id={`${idSchema.$id}__title`}
title={title}
required={required}
formContext={formContext}/> : null}
{schema.description ?
<DescriptionField
id={`${idSchema.$id}__description`}
description={schema.description}
formContext={formContext}/> : null}
{
layout.map((row, index) => {
return (
<div className="row" key={index}>
{
Object.keys(row).map((name, index) => {
const { doShow, ...rowProps } = row[name]
let style = {}
if (doShow && !doShow({ formData })) {
style = { display: 'none' }
}
if (schema.properties[name]) {
return (
<Col {...rowProps} key={index} style={style}>
<SchemaField
name={name}
required={this.isRequired(name)}
schema={schema.properties[name]}
uiSchema={uiSchema[name]}
errorSchema={errorSchema[name]}
idSchema={idSchema[name]}
formData={formData[name]}
onChange={this.onPropertyChange(name)}
onBlur={onBlur}
registry={this.props.registry}
disabled={disabled}
readonly={readonly}/>
</Col>
)
} else {
const { render, ...rowProps } = row[name]
let UIComponent = () => null
if (render) {
UIComponent = render
}
return (
<Col {...rowProps} key={index} style={style}>
<UIComponent
name={name}
formData={formData}
errorSchema={errorSchema}
uiSchema={uiSchema}
schema={schema}
registry={this.props.registry}
/>
</Col>
)
}
})
}
</div>
)
})
}</fieldset>
)
}
}
in the file, you can define doShow property to define whether to show another component.
Next.Define the isFilled function in JsonChemaForm
const isFilled = (fieldName) => ({ formData }) => (formData[fieldName] && formData[fieldName].length) ? true : false
Third,after you choose the first dropdown ,the second dropdown will show up
import LayoutField from './layoutField.js'
const fields={
layout: LayoutField
}
const uiSchema={
"ui:field": 'layout',
'ui:layout': [
{
groups: {
'ui:autofocus': true,
'ui:options': {
size: {
lg: 15,
},
},
}
},
{
users: {
'ui:options': {
size: {
lg: 15,
},
},
doShow: isFilled('groups')
}
}
]
}
...
render() {
return (
<div>
<Form
schema={schema}
uiSchema={uiSchema}
fields={fields}
/>
</div>
)
}