I have a problem with the react-select lib (see here: https://www.npmjs.com/package/react-select). For the validation of my form, I display an error message on the onBlur event. The problem is that no value appears in my logs.
However, onChange works fine.
Handler
const handleBlur = (e: FocusEvent<HTMLInputElement, Element>) => {
//Here, When I select a value and deselect the input, no value exists in this log.
console.log("value Select: ", e.target.value )
}
The return of my component function
<Select
placeholder={`Select ${name}`}
name={name}
id={id}
onBlur={(e) => {
handleBlur(e)
}}
onChange={(e) => {
setValueOnChange(e, name)
}}
options={options}
styles={customStyle}
/>
Anyone have a suggestion?
Thanks !
import "./styles.css";
import Select from "react-select";
import { useState } from "react";
export default function App() {
const options = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
];
const [value, setValue] = useState();
const [focusValue, setFocusValue] = useState();
const handleChange = (changeValue) => {
setValue(changeValue);
};
const handleFocus = (event) => {
const focusValue = event.target.value;
console.log("Should be focus value", focusValue);
setFocusValue(focusValue);
};
const handleBlur = (event) => {
const blurValue = event.target.value;
console.log("Should be blur value", blurValue);
if (focusValue !== blurValue) {
console.log("Do something");
}
};
return (
<div className="App">
<h1>React Select onFocus & onBlur </h1>
<Select
options={options}
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</div>
);
}
Related
Why when user click "remove button" in ListItem component, the item text are disappear but the button itself are still there, if possible how to get rid of that "remove button" too?
P.S The atom family item are got removed but the ui are not get updated ("remove button" are still there), is that a normal things?
import React, {useState} from "react";
import { atom, useRecoilState, useResetRecoilState, useRecoilCallback, atomFamily, useRecoilValue, useSetRecoilState } from "recoil";
import "./styles.css";
const idsState = atom({
key: "circleColor",
default: [],
});
const noteState = atomFamily({
key: "noteState",
default: []
})
const ListItem = ({ id }) => {
const [note, setNote] = useRecoilState(noteState(id));
const handleRemoveNote = useResetRecoilState(noteState(id));
return (
<div key={note.id} className="list-item">
<p>{note.text}</p>
<button onClick={handleRemoveNote}>Remove</button>
</div>
)
}
const App = () => {
const ids = useRecoilValue(idsState);
const nextId = ids.length;
const addNote = useRecoilCallback(({set}) => (newNote) => {
set(idsState, [...ids, nextId])
set(noteState(nextId), newNote);
})
const [text, setText] = useState("");
const handleAddNote = (e) => {
e.preventDefault();
const id = Math.round(Math.random() * 1000);
const newNote = {
text,
id,
subNote: [
{
label: "zero",
value: "0"
},
{
label: "one",
value: "1"
},
{
label: "two",
value: "two"
}
]
};
addNote(newNote);
}
return (
<div>
<form className="form-container" onSubmit={handleAddNote}>
<input onChange={e => setText(e.target.value)} />
<button>Add</button>
</form>
<div>
{ids.map(id => (
<ListItem id={id} />
))}
</div>
</div>
);
};
export default App;
I'm attempting to test a Select input inside an Ant Design Form filled with initialValues and the test is failing because the Select does not receive a value. Is there a best way to test a "custom" rendered select?
Test Output:
Error: expect(element).toHaveValue(chocolate)
Expected the element to have value:
chocolate
Received:
Example Test:
import { render, screen } from '#testing-library/react';
import { Form, Select } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => children,
...options,
});
describe('select tests', () => {
it('renders select', () => {
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
customRender(
<Form initialValues={{ formSelectItem: value }}>
<Form.Item label="Form Select Label" name="formSelectItem">
<Select options={options} />
</Form.Item>
</Form>,
);
expect(screen.getByLabelText('Form Select Label')).toHaveValue(value);
});
});
testing a library component may be harsh sometimes because it hides internal complexity.
for testing antd select i suggest to mock it and use normal select in your tests like this:
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const Select = ({ children, onChange, ...rest }) => {
return <select role='combobox' onChange={e => onChange(e.target.value)}>
{children}
</select>;
};
Select.Option = ({ children, ...otherProps }) => {
return <option role='option' {...otherProps}}>{children}</option>;
}
return {
...antd,
Select,
}
})
this way you can test the select component as a normal select (use screen.debug to check that the antd select is mocked)
I mocked a normal select and was able to get everything working.
The following example utilizes Vitest for a test runner but should apply similar to Jest.
antd-mock.tsx
import React from 'react';
import { vi } from 'vitest';
vi.mock('antd', async () => {
const antd = await vi.importActual('antd');
const Select = props => {
const [text, setText] = React.useState('');
const multiple = ['multiple', 'tags'].includes(props.mode);
const handleOnChange = e => props.onChange(
multiple
? Array.from(e.target.selectedOptions)
.map(option => option.value)
: e.target.value,
);
const handleKeyDown = e => {
if (e.key === 'Enter') {
props.onChange([text]);
setText('');
}
};
return (
<>
<select
// add value in custom attribute to handle async selector,
// where no option exists on load (need to type to fetch option)
className={props.className}
data-testid={props['data-testid']}
data-value={props.value || undefined}
defaultValue={props.defaultValue || undefined}
disabled={props.disabled || undefined}
id={props.id || undefined}
multiple={multiple || undefined}
onChange={handleOnChange}
value={props.value || undefined}
>
{props.children}
</select>
{props.mode === 'tags' && (
<input
data-testid={`${props['data-testid']}Input`}
onChange={e => setText(e.target.value)}
onKeyDown={handleKeyDown}
type="text"
value={text}
/>
)}
</>
);
};
Select.Option = ({ children, ...otherProps }) => (
<option {...otherProps}>{children}</option>
);
Select.OptGroup = ({ children, ...otherProps }) => (
<optgroup {...otherProps}>{children}</optgroup>
);
return { ...antd, Select };
});
utils.tsx
import { render } from '#testing-library/react';
import { ConfigProvider } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => <ConfigProvider prefixCls="bingo">{children}</ConfigProvider>,
...options,
});
export * from '#testing-library/react';
export { default as userEvent } from '#testing-library/user-event';
export { customRender as render };
Select.test.tsx
import { Form } from 'antd';
import { render, screen, userEvent } from '../../../test/utils';
import Select from './Select';
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
const initialValues = { selectFormItem: value };
const renderSelect = () => render(
<Form initialValues={initialValues}>
<Form.Item label="Label" name="selectFormItem">
<Select options={options} />
</Form.Item>
</Form>,
);
describe('select tests', () => {
it('renders select', () => {
render(<Select options={options} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('renders select with initial values', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
});
it('handles select change', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
userEvent.selectOptions(screen.getByLabelText('Label'), 'vanilla');
expect(screen.getByLabelText('Label')).toHaveValue('vanilla');
});
});
I have a problem with set State in dropdown semantic-ui-react.
I am using typescript in my code.
The selected category value doesn't change and always returns an empty string "". How can I fix this?
import debounce from "lodash.debounce";
import { observer } from "mobx-react-lite";
import React, { SyntheticEvent, useContext, useState } from "react";
import {
Dropdown,
DropdownItemProps,
DropdownProps,
Form,
InputOnChangeData,
Popup,
} from "semantic-ui-react";
import { RootStoreContext } from "../../../app/stores/rootStore";
const regex = new RegExp("^[a-zA-Z0-9 ]+$");
interface IProps {
loading: boolean;
}
const PurchaseDetailsFilter: React.FC<IProps> = ({ loading }) => {
const rootStore = useContext(RootStoreContext);
const {
setFilter,
itemCount,
loadPurchaseDetails,
categoryId,
setCategoryId,
} = rootStore.purchaseDetailStore;
const { purchaseCategories } = rootStore.purchaseCategoryStore;
const purchaseCategoriesList: any = purchaseCategories.map((data) => {
return { key: data.id, text: data.name, value: data.id };
});
const categoryOptions: DropdownItemProps[] = [
{ key: "all", value: "all", text: "All" },
].concat(purchaseCategoriesList);
const [selectedCategory, setSelectedCategory] = useState("");
const [filterValid, setFilterValid] = useState(true);
const f = debounce((value: string) => {
if (value !== "" && !regex.test(value)) {
setFilterValid(false);
setFilter(value);
} else {
setFilterValid(true);
console.log(loading);
setFilter(value);
loadPurchaseDetails();
}
}, 500);
const cat = debounce((value: string) => {
console.log(value);
setSelectedCategory(value as string);
console.log(selectedCategory);
setCategoryId((value ==="all" ? "" : value) as string);
loadPurchaseDetails();
}, 500);
const handleOnChange = (
event: React.ChangeEvent<HTMLInputElement>,
{ value }: InputOnChangeData
) => {
f(value);
};
let popupMessage = "";
if (!filterValid) {
popupMessage = "Invalid character.";
} else if (itemCount === 0) {
popupMessage = "No results found.";
}
const handleSelectedCategory = (
event: SyntheticEvent<HTMLElement>,
{value}: DropdownProps
) => {
console.log(value);
setSelectedCategory(String(value));
console.log(selectedCategory);
setCategoryId((value ==="all" ? "" : value) as string);
loadPurchaseDetails();
};
return (
<Form>
<Form.Group>
<Form.Field>
<Popup
trigger={
<Form.Input
placeholder={"Enter a filter."}
name={"filter"}
error={!filterValid}
label={"Filter"}
onChange={handleOnChange}
icon={"search"}
loading={loading}
/>
}
content={popupMessage}
on={"click"}
open={!filterValid || itemCount === 0}
position={"right center"}
/>
</Form.Field>
<Form.Field style={{ marginLeft: "10em" }}>
<label>Category</label>
<Dropdown
//defaultValue="All"
//value={categoryId}
value={selectedCategory}
onChange={handleSelectedCategory}
//defaultValue={categoryOptions[0].value}
placeholder="Choose an category"
options={categoryOptions}
selection
/>
</Form.Field>
<Form.Field>
<label>Project</label>
<Dropdown placeholder="Choose an option" />
</Form.Field>
<Form.Field>
<label>Supplier</label>
<Dropdown placeholder="Choose an option" />
</Form.Field>
</Form.Group>
</Form>
);
};
export default observer(PurchaseDetailsFilter);
maybe what you are looking for is this one:
const handleSelectedCategory = (
event: SyntheticEvent<HTMLElement>,
{value}: DropdownProps
) => {
console.log(value);
setSelectedCategory(String(value));
console.log(selectedCategory);
setCategoryId(String(value ==="all" ? "" : value));
loadPurchaseDetails();
};
notice that I changed value as string to be String(value).
I once fiddle around with TypeScript, but I forget how as string casting works. You might find better explanation here: https://stackoverflow.com/a/32607656/7467018
Have a look at the below code comment
const handleSelectedCategory = (
event: SyntheticEvent<HTMLElement>,
{ value }: DropdownProps
) => {
console.log(value); // If this value is string i.e "Hello World", then just call setSelectedCategory(value);
setSelectedCategory(value as string); // Not required. Just call setSelectedCategory(value) if value is string.
console.log(selectedCategory);
setCategoryId((value === "all" ? "" : value) as string); // Curious, where is setCategoryId froming from?
loadPurchaseDetails();
};
I have the following example component that uses multiple checkboxes for choosing what items to remove from a list of objects:
import React, { useState } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState({});
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
console.log(isChecked);
const newData = data.filter(
item => !Object.keys(isChecked).includes(item.name)
);
console.log(newData);
setFormData(newData);
};
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={allChecked ? true : isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
This is mostly working, except for check all. It seems onChange will not update while using useState. I need to be able to select all the objects or uncheck some to mark for deletion.
Any help is greatly appreciated.
CodeSandbox Example: https://codesandbox.io/s/modest-hodgkin-kryco
UPDATE:
Okay, after some help from Richard Matsen,
Here is a new solution without direct DOM manipulation:
import React, { useState, useEffect } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState();
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
const itemList = Object.keys(isChecked).map((key:any) => {
if (isChecked[key] === true) {
return key
}
})
const result = formData.filter((item:any) => !itemList.includes(item.name))
console.log(result)
setFormData(result)
}
useEffect(() => {
if (!loading) {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}
}, [allChecked, loading]);
useEffect(() => {
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
setIsChecked(initialIsChecked)
setLoading(false)
}, [loading])
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{!loading ? formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
)): null}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
codesandbox of working solution:
https://codesandbox.io/s/happy-rubin-5zfv3
The basic problem is checked={allChecked ? true : isChecked[test.name]} stops the unchecking action from happening - once allChecked is true it does not matter what value isChecked[test.name] has, the expression is always going to be true.
You should rely only on isChecked for the value, and treat changing allChecked as a side-effect.
useEffect(() => {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}, [allChecked]);
...
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
There's also this warning cropping up
Warning: A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
So that's basically saying don't initialize isChecked to {}, because the input's checked property is initially undefined. Use this instead,
{
test1: false,
test2: false,
test3: false,
test4: false,
test5: false,
}
or this way
const data = { ... }
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState(initialIsChecked);
...
The problem with your code was how you were handling allChecked. I have made some changes to your code and it works now.
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
function App() {
const [allChecked, setAllChecked] = useState(false);
// using an array to store the checked items
const [isChecked, setIsChecked] = useState([]);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
if (allChecked) {
setAllChecked(false);
return setIsChecked([]);
}
setAllChecked(true);
return setIsChecked(formData.map(data => data.name));
};
const handleSingleCheck = e => {
const {name} = e.target;
if (isChecked.includes(name)) {
setIsChecked(isChecked.filter(checked_name => checked_name !== name));
return setAllChecked(false);
}
isChecked.push(name);
setIsChecked([...isChecked]);
setAllChecked(isChecked.length === formData.length)
};
const onDelete = () => {
const data_copy = [...formData];
isChecked.forEach( (checkedItem) => {
let index = formData.findIndex(d => d.name === checkedItem)
delete data_copy[index]
}
)
setIsChecked([])
// filtering out the empty elements from the array
setFormData(data_copy.filter(item => item));
setAllChecked(isChecked.length && isChecked.length === data.length);
};
return (
<div className="App">
<form>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
{ formData.map((test, index) => (
<div
key={index}
>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked.includes(test.name)}
onChange={handleSingleCheck}
/>
</div>
))
}
<label />
</form>
<button onClick={onDelete}>DELETE</button>
</div>
);
}
I think you should merge allChecked and isChecked state vars, because they represent the same thing, but your denormalizing it by creating two different vars! I suggest to keep isChecked, and modify all its entries when you press the allChecked input. Then, you can use a derived var allChecked (defined in your component or by using useMemo hook) to know if all your checks are checked or not.
Well, after some time working I came up with:
import React, { useState } from "react";
import "./styles.css";
import { useFormInputs } from "./checkHooks";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [fields, handleFieldChange] = useFormInputs({
checkedAll: false
});
const allcheck = () => {
const checkdata = document.querySelectorAll(".checkers").length;
const numChecks = Array.from(new Array(checkdata), (x, i) => i);
numChecks.map(item => {
console.log(item);
async function checkThem() {
let element = await document.getElementsByClassName("checkers")[item];
element.click();
}
return checkThem();
});
};
return (
<div className="App">
<div>
<label>All</label>
<input name="checkall" type="checkbox" onChange={allcheck} />
<label />
</div>
{data.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
className="checkers"
type="checkbox"
name={test.name}
onChange={handleFieldChange}
/>
</div>
))}
</div>
);
}
Relevent codesandbox: https://codesandbox.io/s/admiring-waterfall-0vupo
Any suggestions welcomed. Also, thanks for the help guys!
This is register form, I want to send the value orgTypes>name to orgType of data of same object using onChange of select.
https://codesandbox.io/s/react-typescript-zm1ov?fontsize=14&fbclid=IwAR06ZifrKrDT_JFb0A-d_iu5YaSyuQ9qvLRgqS20JgAcSwLtAyaOFOoj5IQ
When I use onChange of Select, it erases all other data of the inputs.
import React from "react";
import { Select } from "antd";
const { Option } = Select;
const RegisterFinal = () => {
const data = {
orgName: "",
orgRegNo: "",
orgType: "",
orgTypes: [
{ id: "1", name: "Vendor" },
{ id: "2", name: "Supplier" },
{ id: "3", name: "Vendor and Supplier" }
],
errors: {}
};
const [values, setValues] = React.useState(data);
const handleChange = (e: any) => {
e.persist();
setValues((values: any) => ({
...values,
[e.target.name]: e.target.value
}));
};
const handleSubmit = () => {
console.log(values);
};
return (
<React.Fragment>
<input
name="orgName"
onChange={handleChange}
placeholder="Organization Name"
value={values.orgName}
/>
<input
name="orgRegNo"
onChange={handleChange}
placeholder="Registration Number"
value={values.orgRegNo}
/>
<Select //imported from antd
id="select"
defaultValue="Choose"
onChange={(select: any) => {
setValues(select);
console.log(select); //shows Vender/Supplier, I want this value to be sent to orgType above
}}
>
{data.orgTypes.map((option: any) => (
<Option key={option.id} value={option.name}>
{option.name}
</Option>
))}
</Select>
</React.Fragment>
);
};
When I use onChange of Select, it erases all other data of the inputs.
Thank you for your help
setValues function expects to take the values object as an argument. Instead you are passing the select value to it.
Instead, you can do it like this:
const handleSelectChange = (selectValue: any) => {
setValues((values: any) => ({
...values,
orgType: selectValue
}));
}
and use handleSelectChange in the onChange of your select.
Check the solution here: https://codesandbox.io/s/react-typescript-p9bjz