I am having an app with two sections. Left section contains the categories and the right section containing the items under it. Under each category, I have the button to select all or unselect all items. I see the state changes happening in the code ( it is pretty printed inside HTML) but the checkbox values are not getting updated. Can someone help?
https://codesandbox.io/s/zealous-carson-dy46k8?file=/src/App.js
export const RightSection = ({ name, apps, json, setJson }) => {
function handleSelectAll(categoryName, type) {
const checked = type === "Select All" ? true : false;
const updated = Object.fromEntries(
Object.entries(json).map(([key, category]) => {
if (category.name !== categoryName) {
return [key, category];
}
const { name, tiles, ...rest } = category;
return [
key,
{
name,
...rest,
tiles: tiles.map((item) => ({
...item,
checked
}))
}
];
})
);
setJson(updated);
}
return (
<>
<div>
<input
type="button"
value={`select all under ${name}`}
onClick={() => handleSelectAll(name, "Select All")}
/>
<input
type="button"
value={`unselect all under ${name}`}
onClick={() => handleSelectAll(name, "Unselect All")}
/>
<h4 style={{ color: "blue" }}>{name} Items</h4>
{apps.map((app) => {
return (
<section key={app.tileName}>
<input checked={app.checked} type="checkbox" />
<span key={app.tileName}>{app.tileName}</span> <br />
</section>
);
})}
</div>
</>
);
};
import { useEffect, useState, useMemo } from "react";
import { SidebarItem } from "./SideBarItem";
import { RightSection } from "./RightSection";
import "./styles.css";
export default function App() {
const dummyJson = useMemo(() => {
return {
cat1: {
id: "cat1",
name: "Category 1",
tiles: [
{
tileName: "abc",
searchable: true,
checked: false
},
{
tileName: "def",
searchable: true,
checked: true
}
]
},
cat2: {
id: "cat2",
name: "Category 2",
tiles: [
{
tileName: "ab",
searchable: true,
checked: true
},
{
tileName: "xyz",
searchable: true,
checked: false
}
]
},
cat3: {
id: "cat3",
name: "Category 3",
tiles: [
{
tileName: "lmn",
searchable: true,
checked: true
},
{
tileName: "",
searchable: false,
checked: false
}
]
}
};
}, []);
const [json, setJson] = useState(dummyJson);
const [active, setActive] = useState(dummyJson["cat1"]);
return (
<>
<div className="container">
<div>
<ul>
{Object.values(json).map((details) => {
const { id, name } = details;
return (
<SidebarItem
key={name}
name={name}
{...{
isActive: id === active.id,
setActive: () => setActive(details)
}}
/>
);
})}
</ul>
</div>
<RightSection
name={active.name}
apps={active.tiles}
{...{ json, setJson }}
/>
</div>
<p>{JSON.stringify(json, null, 2)}</p>
</>
);
}
since you have not updated data of checkbox (in your code) / logic is wrong (in codesandbox) do the following add this function in RightSection
...
function setTick(app, value: boolean) {
app.checked = value;
setJson({...json})
}
...
and onChange in input checkbox
<input
onChange={({ target }) => setTick(app, target.checked)}
checked={app.checked}
type="checkbox"
/>
Codesandbox: see line 25 -> 28 and line 48 in RightSection.tsx are the lines I added
For the two buttons select all and unselect all to update the state of the checkboxes, the data must be synchronized (here you declare active as json independent of each other, this makes the update logic complicated. Unnecessarily complicated, please fix it to sync
const [json, setJson] = useState(dummyJson);
const [activeId, setActiveId] = useState('cat1');
const active = useMemo(() => json[activeId], [json, activeId]);
and update depends:
<SidebarItem
key={name}
name={name}
{...{
isActive: id === activeId,
setActive: () => setActiveId(id)
}}
/>
Codesandbox: line 60 -> 63 and line 81 -> 82 in file App.js
https://codesandbox.io/s/musing-rhodes-yp40fi
the handleOperation function could also be rewritten very succinctly but that is beyond the scope of the question
Related
I'm new to learning react so I followed this tutorial https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_components to create a todo app and then tweaked it to fit the requirements of the project I'm working on. Everything works the way it should except when I delete (complete) things from the associate side, it also deletes it from my main side as well. I understand the general concept of why that's happening (I don't have two separate lists in my code), just not sure how to go about fixing it without removing the filter I have in place. I had tried to implement a separate list for those tasks but just wasn't understanding how to go about it.
Added CodeSandBox for more context: https://codesandbox.io/s/hungry-sky-5f482?file=/src/index.js
Check task items and then view the items you've checked in "Show Associate Tasks." The issue is completing a task on the associate side also completes it on the "Show All Tasks" side.
App.js
const FILTER_MAP = {
All: () => true,
Associate: task => task.checked
};
const FILTER_NAMES = Object.keys(FILTER_MAP);
function App(props) {
const [tasks, setTasks] = useState(props.tasks);
const [filter, setFilter] = useState('All');
function toggleTaskChecked(id) {
const updatedTasks = tasks.map(task => {
if (id === task.id) {
return {...task, checked: !task.checked}
}
return task;
});
setTasks(updatedTasks);
}
function completeTask(id) {
const remainingTasks = tasks.filter(task => id !== task.id);
setTasks(remainingTasks);
}
const taskList = tasks
.filter(FILTER_MAP[filter])
.map(task => (
<Todo
id={task.id}
name={task.name}
checked={task.checked}
key={task.id}
toggleTaskChecked={toggleTaskChecked}
completeTask={completeTask}
/>
));
const filterList = FILTER_NAMES.map(name => (
<FilterButton
key={name}
name={name}
isPressed={name === filter}
setFilter={setFilter}
/>
));
function addTask(name) {
const newTask = { id: "todo-" + nanoid(), name: name, checked: false };
setTasks([...tasks, newTask]);
}
return (
<div className="app">
<h1 className = "tasks-header">Task Tracker</h1>
<Form addTask={addTask}/>
<div className="list-buttons">
{filterList}
</div>
<ul
role="list"
className="todo-list"
aria-labelledby="list-heading"
>
{taskList}
</ul>
</div>
);
}
export default App
Todo.js
export default function Todo(props) {
return (
<li className="todo stack-small">
<div className="c-cb">
<input id={props.id}
type="checkbox"
defaultChecked={props.checked}
onChange={() => props.toggleTaskChecked(props.id)}
/>
<label className="todo-label" htmlFor="todo-0">
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button"
className="complete-button"
onClick={() => props.completeTask(props.id)}
>
Complete
</button>
</div>
</li>
);
}
index.js
const DATA = [
{ id: "todo-0", name: "Brush Teeth", checked: false },
{ id: "todo-1", name: "Make Dinner", checked: false },
{ id: "todo-2", name: "Walk Dog", checked: false },
{ id: "todo-3", name: "Run Reports", checked: false },
{ id: "todo-4", name: "Visit Mom", checked: false },
{ id: "todo-5", name: "Aerobics", checked: false },
{ id: "todo-6", name: "Project", checked: false },
{ id: "todo-7", name: "Lecture", checked: false },
{ id: "todo-8", name: "Have Lunch", checked: false }
];
ReactDOM.render(
<React.StrictMode>
<App tasks={DATA}/>
</React.StrictMode>,
document.getElementById('root')
);
FilterButton.js
function FilterButton(props) {
return (
<button
type="button"
className="toggle-btn"
aria-pressed={props.isPressed}
onClick={() => props.setFilter(props.name)}
>
<span className="visually-hidden">Show </span>
<span>{props.name}</span>
<span className="visually-hidden"> Tasks</span>
</button>
);
}
export default FilterButton;
We have 3 boolean fields: checked, completed, completedAssoc.
{
id: "todo-0",
name: "Brush Teeth",
checked: false,
completed: false,
completedAssoc: false
},
The filters will work as follows:
const FILTER_MAP = {
All: (task) => !task.completed,
Associate: (task) => task.checked && !task.completedAssoc
};
And finally changes in completeTask and addTask:
function completeTask(id) {
const complField = filter === "All" ? "completed" : "completedAssoc";
const updatedTasks = tasks.map((task) =>
id === task.id ? { ...task, [complField]: true } : task
);
setTasks(updatedTasks);
}
function addTask(name) {
const newTask = {
id: "todo-" + nanoid(),
name: name,
checked: false,
completed: false,
completedAssoc: false
};
setTasks([...tasks, newTask]);
}
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 this component:
import React from 'react';
const options = [
{ label: "Lifestyle", value: "lifestyle"},
{ label: "Area", value: "area" },
{ label: "Random", value: "random" }
];
const ChannelCategory = props =>
props.visible ? (
<div>
{props.title}
<ul>
{options.map((option) => (
<li key={option.value}>
<label>
{option.label}
<input
className={props.className}
name={props.name} // need to be different
selected={props.selected === option.value} // e.g. lifestyle === lifestyle
onChange={() => props.onChange(option.value)}
type="radio"
/>
</label>
</li>
))}
</ul>
</div>
) : null;
export default ChannelCategory;
I am rendering it on another page here in a .map:
let displayExistingChannels = null;
if (channels !== null){
displayExistingChannels = (
channels.map(channel => {
return (
<Grid key={channel.key} item style={styles.gridItem} justify="space-between">
<ChannelListItem
channel={channel}
isSaving={isSaving}
onDeleteChannelClick={onDeleteChannelClick}
key={channel.key}
onFormControlChange={onFormControlChange}
onUndoChannelClick={onUndoChannelClick}
/>
{channel.category}
<ChannelCategory
visible={true}
onChange={value => setCategoryName(value)}
title="Edit Category"
selected={channel.category}
name={channel.key} // unique for every channel
/>
</Grid>
)
})
)
}
I am using fake data for the map:
const fakeChannelData = setupChannels(
[{id: "2f469", name: "shopping ", readOnly: false, category: "lifestyle"},
{id: "bae96", name: "public", readOnly: true, category: "null"},
{id: "06ea6", name: "swimming ", readOnly: false, category: "sport"},
{id: "7e2bb", name: "comedy shows ", readOnly: false, category: "entertainment"}]);
const [channels, setChannels] = useState(fakeChannelData);
Please can someone tell me why when I add selected={channel.category} in my .map function it does not show the selected category preselected on the FE on page load? Not sure where I have gone wrong? Thanks!
checked is the correct attribute to use for input tag, not selected.
<input
...
checked={props.selected === option.value}
...
/>
ref: https://developer.mozilla.org/fr/docs/Web/HTML/Element/Input/radio
Hello I have doubts on how I can do this in react using useState,
basically i have this menu where i need to map, i basically need a state containing all tags, and with boolean state true or false to know if the current item is active, and i will make it active by clicking on the item, and deactivate it when another item is clicked
that is, only one menu item active at a time
export const SideBarTags = [
{
name: 'Tutoriais',
link: '../tutorials',
icon: faFileAlt,
dropdownItems: null,
active: false,
},
{
name: 'Avisos',
link: '../news',
icon: faNewspaper,
dropdownItems: null,
active: false,
},
{
name: 'Serviços',
link: '../services',
icon: faMeteor,
active: false,
dropdownItems: [
{ name: 'Elo Boost', link: '/eloBost' },
{ name: 'Duo Boost', link: '/duoBoost' },
{ name: 'MD10', link: '/eloBost' },
{ name: 'Coaching', link: '/duoBoost' },
{ name: 'Vitóriais', link: '/duoBoost' },
],
},
{
name: 'Carteira',
link: '../cartcredit',
icon: faWallet,
active: false,
dropdownItems: [
{ name: 'Histórico', link: '/history' },
{ name: 'Adicionar Crédito', link: '/add' },
],
},
];
and my TSX:
const MenuTags: React.FC<Hamburguer> = ({ isOpen }) => {
const [menuTags, setMenuTags] = useState(SideBarTags.map());
return (
<DashMenu open={isOpen}>
<MenuItem /> //(this is my tag <li>
</DashMenu>
);
};
const MenuItem: React.FC = () => {
return (
<ListItem>
<ListWrap
>
<a>
<FontAwesomeIcon
className="icon-li"
icon={icon}
size={isOpen ? 'lg' : 'lg'}
fixedWidth
color="white"
/>
<span
className="li-name"
>
{name}
</span>
</a>
</ListItem>
);
};
Component logic if you wanted to map the menu items with the active item
const [menuItems, setMenuItems] = useState(SideBarTags);
const clickHandler = name => () => {
setMenuItems(items =>
items.map(item => ({
...item,
active: item.name === name
}))
);
};
...
{menuItems.map(item => (
<li
key={item.name}
className={item.active ? "active" : ""}
onClick={clickHandler(item.name)}
>
{item.name}
</li>
))}
CSS
.active {
// could be whatever style you need
color: red;
}
Super simplified version of what I did in a past project:
const MenuTags = () => {
const [selectedLink, setSelectedLink] = useState(null)
return (
<ul>
{SideBarTags.map((obj) => (
<li className={`${selectedLink === obj.name ? 'link--selected' : ''}`}>
<a
onClick={() => {
setSelectedLink(obj.name)
}}
href={obj.link}
>
{obj.name}
</a>
</li>
))}
</ul>
)
}
Use CSS to open and close the menu items, by having a class such as 'link--selected' added to an element you can just show that item.
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>
)
}