display input fields in customized way - javascript

I have a dynamic form. My input fields belong to different groups. I want to figure out a way to display them group-wise.
My state through which form input fields are generated is:
random:
{
name: "emp1", group: "employee",
name: "emp2", group: "employee",
name: "emp3", group: "employee",
name: "man1", group: "manager",
name: "man2", group: "manager"
}
My dynamic form is as follows:
const Form = (props) => {
return (
<div>
{props.items.map(item => (
name={item.key_name}
value={item.key_value}
onChange={e => props.handleChange(e)}
/>
)
)}
<button onClick={() => props.handleSubmit()} >
Submit
</button>
</div>
)
}
export default Form
right now my fields are generated side by side in a horizontal line
But I want to display them group wise; so something like this:
Employee: (3 input fields)
Manager: (2 input fields)

You just need to split your items by their group, then render them. I would make the group a component so it handles rendering the label and also the inputs
const Form = props => {
const itemsByGroup = {};
props.items.forEach(item => {
if (!itemsByGroup[item.group]) {
itemsByGroup[item.group] = [];
}
itemsByGroup[item.group].push(item);
});
return (
<div>
{Object.keys(itemsByGroup).map(groupName => (
<FormGroup
label={groupName}
fields={itemsByGroup[groupName]}
handleChange={props.handleChange}
key={groupName}
/>
))}
<button onClick={() => props.handleSubmit()}>Submit</button>
</div>
);
};
const FormGroup = ({ label, fields, handleChange }) => (
<div>
<label>{label}:</label>
{fields.map(field => (
<input
name={field.key_name}
value={field.key_value}
onChange={e => handleChange(e)}
/>
))}
</div>
);
See a live example here to play with
Edit
if you have to put it in one component you can just do this
const Form = props => {
const itemsByGroup = {};
props.items.forEach(item => {
if (!itemsByGroup[item.group]) {
itemsByGroup[item.group] = [];
}
itemsByGroup[item.group].push(item);
});
return (
<div>
{Object.keys(itemsByGroup).map(groupName => (
<div>
<label>{label}:</label>
{fields.map(field => (
<input
name={field.key_name}
value={field.key_value}
onChange={e => handleChange(e)}
/>
))}
</div>
))}
<button onClick={() => props.handleSubmit()}>Submit</button>
</div>
);
};

Related

state change not triggering a re-render

I have a component call ExpenseList.js which does look like below. But my problem is when I tried to edit item and click save, setting isEditable inside "Save" button event handler does not trigger re-render.
import { useState } from "react";
import { useExpenses, useExpenseDispatch } from "./BudgetContext.js";
export default function ExpenseList() {
const expenses = useExpenses();
return (
<ul>
{expenses.map((expense) => (
<li key={expense.id}>
<Expense expense={expense} />
</li>
))}
</ul>
);
}
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useExpenseDispatch();
let content;
if (isEditing) {
content = (
<>
<input
value={expense.description}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
description: e.target.value
}
});
}}
/>
<input
value={expense.amount}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
amount: e.target.value
}
});
}}
/>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<span>{expense.description}</span> : <span>{expense.amount}</span>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<label>
{content}
<button
onClick={() => {
dispatch({
type: "deleted",
id: expense.id
});
}}
>
Delete
</button>
</label>
);
}
I was dabbling with this for hours, I think extra pair of eyes could point out what is going wrong?
Sandbox: https://codesandbox.io/s/clever-keller-l5z42e?file=%2FExpenseList.js%3A0-1614
Documentation reference
Content model:
Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.
As mentioned above, label has 2 different labelable elements unless it is the element's labeled control. When you are in edit mode, you have 3 different labelable elements (input-description, input-amount and button-save) which causes problems with event propagation.
But when you are not in edit mode, it just has 1 labelable element which is the edit button and hence, it works.
For solving your issue, you can swap the label at the root with something like div and then use labels explicitly for each of the inputs in content.
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
let content;
if (isEditing) {
content = (
<>
<label>
Description:
<input
value={expense.description}
onChange={...}
/>
</label>
<label>
Amount:
<input
value={expense.amount}
onChange={...}
/>
</label>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<div>
{content}
<button>Delete</button>
</div>
);
}

How to setup dynamic onChange event handler?

I'm working with a react codebase and have to pass different functions in an onChange event handler into another component that fires up dynamically. This is the code in a component managing filter:
<legend>Brands</legend>
{brands.map((data) => (
<>
<CheckboxFilter
key={brands.id}
data={data}
filterByBrand={filterByBrand}
/>
</>
))}
<legend>Display</legend>
{displays.map((data) => (
<>
<CheckboxFilter
key={displays.id}
data={data}
filterByDisplay={filterByDisplay}
/>
</>
))}
And this is another component which manages Checkbox handling:
<fieldset>
<div>
<input
type="checkbox"
name={data}
id={data}
value={data}
onChange={() => {
filterByBrand(data);
}}
/>
<label htmlFor={data}>{data}</label>
</div>
</fieldset>
The issue is, I have to pass both {filterByBrand} & {filterByDisplay} props into the onChange event handler of the Checkbox component dynamically. But when passing, it reads both functions ({filterByBrand} & {filterByDisplay}) instead of reading dynamically.
I would recommend you to refactor your CheckboxFilter component, you don't need props like filterByBrand or filterByDisplay, rename them to onChange or something like, then in your CheckboxFilter you will get always the same name.
Also, I don't know your use case for these filters, but I guess you need 2 additional states to keep the values of selected Brands and Displays
Here is an example of how I would implement these filters:
const CheckboxFilter = ({
id, checked, label, onChange,
}) => (
<fieldset>
<div>
<input
type="checkbox"
checked={checked}
id={id}
onChange={onChange}
/>
<label htmlFor={id}>{label}</label>
</div>
</fieldset>
);
// additional hook to keep state of selected items
const useSelection = () => {
const [ids, setIds] = React.useState([]);
const toggle = React.useCallback((id) => {
if (ids.includes(id)) {
setIds(prev => prev.filter(i => i !== id));
} else {
setIds(prev => [...prev, id]);
}
}, [ids, setIds]);
return { ids, toggle };
};
// sample data
const brands = [{ id: 1, name: 'Brand 1' }, { id: 2, name: 'Brand 2' }];
const displays = [{ id: 1, name: 'Display 1' }, { id: 2, name: 'Display 2' }];
const Filters = () => {
const selectedBrands = useSelection();
const selectedDisplays = useSelection();
// selectedBrands.ids includes selected brands
// selectedDisplays.ids includes selected displays
return (
<div>
<legend>Brands</legend>
{brands.map((brand) => (
<CheckboxFilter
key={brand.id}
id={`brand_${brand.id}`}
checked={selectedBrands.ids.includes(brand.id)}
onChange={() => {
selectedBrands.toggle(brand.id);
}}
/>
))}
<legend>Display</legend>
{displays.map((display) => (
<CheckboxFilter
key={display.id}
id={`display_${display.id}`}
checked={selectedDisplays.ids.includes(display.id)}
onChange={() => {
selectedDisplays.toggle(display.id);
}}
/>
))}
</div>
);
};

Conditional rendering an input inside a map

I have a list of task which inside have another task(subTask).
I use a map to list all the tasks and have a button to add more subtasks to that task.
The input button is supposed to be invisible unless the button is pressed.
The problem is that I used the useState hook to conditional render the input but whenever I click in any of the button, the inputs of all tasks open and is supposed to only open the input from that certain index.
const Task = () => {
const [openTask, setOpenTask] = useState(false);
return task.map((item, index) => {
return (
<div>
<button onClick={() => setOpenTask(!openTask)}>Add</button>
{item.name}
{openTask && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
Try using array
const Task = () => {
const [openTask, setOpenTask] = useState([]);
const openTaskHandler = (id) => {
setOpenTask([id])
}
return task.map((item, index) => {
return (
<div>
<button onClick={() => openTaskHandler(item.id)}>Add</button>
{item.name}
{openTask.includes(item.id) && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
That's because you are setting openTask to true for all the items. You can create object and store the specific id to true.
for eg.
const [openTask, setOpenTask] = useState({});
and then on button onClick you can set the specific id to open
setOpenTask({...openTask,[`${item.name}-${index}`]:true});
and then check against specific id
{openTask[`${item.name}-${index}`] && <input placeholder="Add more tasks..."/>}

Passing selected option's value to another component in nextjs

I'm new to nextjs/react so bare with me here. In my project I have multiple select elements with multiple options in each one. When an option is selected or changed I want to pass that value to another component. I was able to pass an onClick event to another component but when I tried a similar solution I wasn't able to get it to work. So the select elements are being mapped in Component A2, but the options for those elements are also being mapped in Component A3 and I need to pass the value to Component B2. You will see in my code I tried to pass it with the "handleOnChange". I'm not very good at explaining things so here is my code snippets, I hope this makes sense:
Parent Component
export default function Post({ globalProps, page, globalPages, sidebarProps }) {
const [addFlexItem, setAddFlexItem] = useState(false)
const [addFlexItemStyles, setFlexItemStyles] = useState()
return (
<Layout globalProps={globalProps}>
<main className={styles.container}>
<FlexSidebar sidebarProps={sidebarProps} onClick={() => setAddFlexItem(true)} handleOnChange={() => setFlexItemStyles()} />
<FlexContainer addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</main>
</Layout>
)
}
Component A1
const FlexSidebar = ({ sidebarProps, onClick, handleOnChange }) => {
return (
<aside className={styles.left_sidebar}>
<section className={styles.wrap}>
{/* we are padding the onClick to the child component */}
{container === true && <FlexSidebarContainer sidebarProps={sidebarProps} onClick={onClick} handleOnChange={handleOnChange} />}
{items === true && <FlexSidebarItems sidebarProps={sidebarProps} />}
</section>
</aside>
)
}
Component A2
const FlexSidebarContainer = ({ sidebarProps, onClick, handleOnChange }) => {
const options = sidebarProps.options
return (
<>
<p className={styles.warning}>{sidebarProps.containerWarningText}</p>
<button type="button" className="btn" onClick={() => onClick()}>
{sidebarProps.addItemBtn}
</button>
<form className={styles.form}>
{options.map((option, index) => {
return (
<div key={index} className={styles.form_item}>
<div className={styles.form_label_wrap}>
<label>{option.title}</label>
</div>
<FlexSidebarSelect options={option.items} handleOnChange={handleOnChange} />
</div>
);
})}
</form>
</>
)
}
Component A3
const FlexSidebarSelect = ({ options, handleOnChange }) => {
return (
<div className={styles.form_item_wrap}>
<select onChange={(value) => handleOnChange(value)}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}
Component B1
const FlexContainer = ({ addFlexItem, addFlexItemStyles }) => {
return (
<section className={styles.right_content}>
<FlexItem addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</section>
)
}
Component B2
const FlexItem = ({ addFlexItem, addFlexItemStyles }) => {
const [isaddFlexItem, setaddFlexItem] = useState(addFlexItem)
useEffect(() => {
setaddFlexItem(addFlexItem)
}, [addFlexItem])
return (
isaddFlexItem ?
<div className={styles.flex_item}>
<div className={styles.flex_item_wrap}>
<div className={styles.flex_item_inner}>
</div>
<button className={styles.trash}>
</button>
</div>
</div>
: "empty"
)
}
I will add that if I change the code in Component A3 to this, im able to log the value, but I cant get it to work in the parent component.
const FlexSidebarSelect = ({ options, handleOnChange }) => {
const [value, setValue] = useState("")
const handleOptionChange = (e) => {
let value = e.target.value
setValue({
value
})
}
return (
<div className={styles.form_item_wrap}>
<select onChange={handleOptionChange}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}

Adding Dynamic Input Field Within Nested "Correct" Category Group

I'm building a dynamic form where you can add multiple categories and within those categories add multiple input fields
states (is there a better way in doing the states?):
const [fields, setFields] = useState([{ value: null }]);
const [group, setGroups] = useState([{ id: null, cat: null, items: [] }]);
functions:
// This is to add multiple group of fields.
const handleAddGroup = () => {
const values = [...group];
console.log("values", values);
values.push({ id: null, cat: null, items: [] });
setGroups(values);
};
// this is where I'm having trouble,
// it doesn't add the new field within the respected category group.
const handleAddField = (i) => {
const newSetOfField = [...group[i].items];
newSetOfField.push({ value: null });
setFields(newSetOfField);
};
code:
<Segment>
<Header content="Create item" />
<Button primary onClick={() => handleAddGroup()}>
Add Group
</Button>
<Form>
{group.map((group, groupIdx) => {
return (
<Segment key={`${group}-${groupIdx}`}>
<Button primary onClick={() => handleAddField(groupIdx)}>
+
</Button>
<Form.Field>
<input
value={group.cat}
type="text"
placeholder="Category title"
onChange={(e) => handleChangeCategory(groupIdx, e)}
/>
</Form.Field>
{group.items.map((field, fieldIdx) => {
return (
<>
<Form.Field key={`${field}-${fieldIdx}`}>
<input
value={field.value}
onChange={(e) => handleChangeField(fieldIdx, e)}
type="text"
placeholder="Item title"
/>
</Form.Field>
<Button onClick={() => handleRemoveField(fieldIdx)}>
X
</Button>
</>
);
})}
<Button onClick={() => handleRemoveGroup(groupIdx)}>
Remove Group
</Button>
</Segment>
);
})}
</Form>
</Segment>
I'm able to use handleAddGroup() to populate multiple groups of fields. Image shown:
Those + buttons supposed to add input fields under the category title input field.

Categories