Chakra UI - Checkbox Group - reactjs

I am using Chakra UI with React Typescript and implementing a checkbox group
The default values are controlled by an outside state that is passed down as a prop.
The problem is that the CheckboxGroup doesn't accept the default values from outside source
The code is as follows:
import React, {FC, useCallback, useEffect, useState} from "react";
import { CheckboxGroup, Checkbox, VStack } from "#chakra-ui/react";
interface IGroupCheckbox {
values: StringOrNumber[],
labels: StringOrNumber[],
activeValues: StringOrNumber[]
onChange: (value:StringOrNumber[])=> void
}
const GroupCheckbox:FC<IGroupCheckbox> = ({
values,
labels,
activeValues,
onChange
}) => {
const [currActiveValues, setCurrActiveValues] = useState<StringOrNumber[]>();
const handleChange = useCallback((value:StringOrNumber[]) => {
if(value?.length === 0) {
alert('you must have at least one supported language');
return;
}
onChange(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(()=>{
if(activeValues) {
setCurrActiveValues(['en'])
}
},[activeValues])
return (
<CheckboxGroup
onChange={handleChange}
defaultValue={currActiveValues}
>
<VStack>
{values && labels && values.map((item:StringOrNumber, index:number)=>
{
return (
<Checkbox
key={item}
value={item}
>
{labels[index]}
</Checkbox>
)
}
)}
</VStack>
</CheckboxGroup>
)
}
export default GroupCheckbox
When I change the defaultValue parameter, instead of the state managed, to be defaultValue={['en']} it works fine, but any other input for this prop doesn't work.
I checked and triple checked that the values are correct.

Generally, passing a defaultValue prop "from an outside source" actually does work. I guess that in your code, only ['en'] works correctly because you explicitly use setCurrActiveValues(['en']) instead of setCurrActiveValues(activeValues).
The defaultValue prop will only be considered on the initial render of the component; changing the defaultValue prop afterwards will be ignored. Solution: make it a controlled component, by using the value prop instead of defaultValue.
Note that unless you pass a parameter to useState(), you will default the state variable to undefined on that initial render.
Side note: You also don't need a separate state variable currActiveValues. Instead, you can simply use activeValues directly.
const GroupCheckbox = ({ values, labels, activeValues, onChange }: IGroupCheckbox) => {
const handleChange = useCallback(
(value: StringOrNumber[]) => {
if (value?.length === 0) {
alert("you must have at least one supported language")
return
}
onChange(value)
},
[onChange]
)
return (
<CheckboxGroup onChange={handleChange} value={activeValues}>
<VStack>
{labels && values?.map((item: StringOrNumber, index: number) => (
<Checkbox key={item} value={item}>
{labels[index]}
</Checkbox>
))}
</VStack>
</CheckboxGroup>
)
}

Related

Material UI Autocomplete not updating input value (React)

I made an Autocomplete component in React using Material UI's Autocomplete component. Here's the code
import { useState } from "react";
import { Autocomplete as MuiAutcomplete } from "#mui/material";
import {useFormContext} from "react-hook-form";
interface props {
name: string,
options?: string[],
getOptions?: (value: string) => {
label: string,
id: number
}[] | string[],
freeSolo?: boolean
};
const Autocomplete = ({name, options=[], getOptions, freeSolo=false}: props) => {
const [autocompleteValues, setAutocompleteValues] = useState<any[]>(options);
const {setValue, getValues} = useFormContext();
return (
<MuiAutcomplete
options={autocompleteValues}
renderInput={({ InputProps, inputProps }) => (
<div ref={InputProps.ref}>
<input
type="text"
{...inputProps}
className="bg-transparent outline-none p-1"
/>
</div>
)}
value={getValues(name)}
onChange={(e, v) => {
setValue(name, v);
}}
getOptionLabel={(option) => option.label || option}
freeSolo={freeSolo}
/>
)
}
export default Autocomplete;
The options display just fine when I type but when actually selecting an option the input field doesn't actually get updated. It instead shows this error:
`MUI: The value provided to Autocomplete is invalid.None of the options match with `""`.You can use the `isOptionEqualToValue` prop to customize the equality test. `
I'm not entirely sure what's going on. Here's a video showing the error in case you need clarification https://imgur.com/a/xfm1mpb (sorry for low res, Imgur's compression ruined it)
Can you please details about what options are you passing?
It will be better if you can provide a codesandbox.
It is due to the option which you are selecting is not matching the value in options
The "" value is an actual value for auto complete. If you want to make it work with an empty field you can:
// Set value to null. If you set it to 'undefined' it will give you a warning similar to the current one
value={getValues(name) || null}
Or
// Override the function which checks if the value is equal to the option.
// Basically, you handle the empty string here.
isOptionEqualToValue={(option, currentValue) => {
if (currentValue === '') return true;
return option.name === currentValue.name;
}}

Why is useImperitaveHandle hook used in React?

I am not able to find a use Case for useImperativeHandle Hook. Trying to Google and understand I came across a code sandbox showing an example of why the useImperitaveHandle Hook would be used. Here is the link to the codesandbox
https://codesandbox.io/s/useimperativehandle-example-forked-illie?file=/src/App.js
I modified the code to get it working without the useImperitaveHandle in the codesandbox link below. Can someone explain why the hook would be used as I believe that code can be written without it to provide the exact same functionality.
https://codesandbox.io/s/useimperativehandle-example-forked-2cjdc?file=/src/App.js
I found an example where this would be used. According to my understanding it will be mainly needed if you need to write some custom functionality for a library and will need some of the library's in built features.
In the example I will provide, I will write a custom CellEditor for the library ag-grid(Table library) because I want to select the value of the cell in the table using Material UI's autocomplete. Below is the code
import React, { useState, forwardRef, useImperativeHandle } from "react";
import MuiTextField from "#material-ui/core/TextField";
import MuiAutocomplete from "#material-ui/lab/Autocomplete";
const AutocompleteEditor = forwardRef(
({ fieldToSave, fieldToShow, textFieldProps, options, ...props }, ref) => {
const [value, setValue] = useState("");
useImperativeHandle(ref, () => {
return {
getValue: () => {
return value;
},
afterGuiAttached: () => {
setValue(props.value);
},
};
});
const tranformValue = (value, fieldtosave) =>
Array.isArray(value)
? value.map((v) => v[fieldtosave] || v)
: value[fieldtosave];
function onChangeHandler(e, value) {
setValue(value ? tranformValue(value, fieldToSave) : null);
}
return (
<MuiAutocomplete
style={{ padding: "0 10px" }}
options={options}
getOptionLabel={(item) => {
return typeof item === "string" || typeof item === "number"
? props.options.find((i) => i[fieldToSave] === item)[fieldToShow]
: item[fieldToShow];
}}
getOptionSelected={(item, current) => {
return item[fieldToSave] === current;
}}
value={value}
onChange={onChangeHandler}
disableClearable
renderInput={(params) => (
<MuiTextField
{...params}
{...textFieldProps}
style={{ padding: "5px 0" }}
placeholder={"Select " + props.column.colId}
/>
)}
/>
);
}
);
export default AutocompleteEditor;
IGNORE THE AUTOCOMPLETE PART IF ITS CONFUSING, ITS NOT IMPORTANT FOR UNDERSTANDING useImperativeHandler
So To explain what the code does. The value of the above component is set by Autocomplete by the user, but ag-grid table also needs the value as it needs to update the value in corresponding cell.
It uses a ref internally that it passes to the customCellEditor. The useImperativeHook then tells ag-grid to use the value from the state of the component whenever getValue is called (it is called when the cell needs to display the value)

fire an onChange event while setting the initial value

I am trying to build a select field which will "select" an Entry automatically when there is only one entry in the list. My Problem is, that there will no onChange event triggered, when setting the value in the list. Is there any possibility to send these event programatically?
This is my code so far
export const SelectField = function(props) {
const classes = useStyles();
const {t} = useTranslation();
const [selectedValue, setSelectedValue] = React.useState(undefined);
if (selectedValue === undefined && props.menuEntries.length === 1) {
setSelectedValue(props.menuEntries[0]);
//need to fire an event in this case
} else if (props.addSelectEntry) {
props.menuEntries.push({"name":t("select"), "value":""});
}
return (
<Select
value={selectedValue}
onChange={props.onChange()}
name={props.name}
displayEmpty
className={classes.selectEmpty}
>
{props.menuEntries.map(entry => (
<MenuItem key={entry.name} value={entry.value}>
{entry.name}
</MenuItem>
))}
</Select>);
};
There is an important difference in passing a function as a prop and calling a function.
onChange={props.onChange()}
Will call that function every render.
onChange={props.onChange}
Will pass that function to be called by the component.
For one is the mentioned onChange() to onChange, but that is not all.
It seems the issue is connected to code outside of the provided component. One indicator of an issue is the following:
....
} else if (props.addSelectEntry) {
props.menuEntries.push({"name":t("select"), "value":""}); // This line could be faulty
}
....
I don't know what kind of value menuEntries is, but it should be a state somewhere higher in your component tree. You should also pass in the setter. Probably called something like setMenuEntries. Then call that setter in instead of mutate the prop.
setMenuEntries([...menuEntries, {"name":t("select"), "value":""}])
Only when setting a state a rerender is triggered.
In general, updating props of any function is considered a bad-practise when following the principle of immutability. Eslint can help you with signifying patterns like that.
From the looks of your code it seems to be alright, there is however one thing that might cause your problem.
return (
<Select
value={selectedValue}
onChange={props.onChange} <---
name={props.name}
displayEmpty
className={classes.selectEmpty}
>
{props.menuEntries.map(entry => (
<MenuItem key={entry.name} value={entry.value}>
{entry.name}
</MenuItem>
))}
</Select>);
I changed how you set the onchange function. You did it while also calling the actual function. This will make the function fire when the component renders and not on the change of your controlled value for this select.
I have solved my problem by calling he handler by my own, if i set the initial value:
export const SelectField = function(props) {
const classes = useStyles();
const { t } = useTranslation();
const [selectedValue, setSelectedValue] = React.useState(props.value);
// on startup load test cases
React.useEffect(() => {
if (selectedValue === props.value && props.menuEntries.length === 1) {
setSelectedValue(props.menuEntries[0].value);
props.onChange(props.menuEntries[0].value);
}
}, [selectedValue]);
const updateValue = function(e) {
props.onChange(e.target.value);
setSelectedValue(e.target.value);
};
return (
<Select
value={selectedValue}
onChange={updateValue}
name={props.name}
displayEmpty
className={classes.selectEmpty}
>
{
(props.menuEntries.length !== 1 && props.addSelectEntry) && (
<MenuItem key={t("select")} value={t("select")}>
{t("select")}
</MenuItem>
)
}
{props.menuEntries.map(entry => (
<MenuItem key={entry.name} value={entry.value}>
{entry.name}
</MenuItem>
))}
</Select>);
};

How to test functionality of function props in storybook?

I have a parent component, <AssetSelectorMenu>, which is composed of two child components:
export const AssetSelectorMenu = (({ assets, sortByName }) => {
return (
<>
<AssetSelectorHeader sortByName={sortByName} />
{assets && assets.map((asset) => (
<AssetSelectorRow key={asset} />
))}
</>
);
});
storybook for AssetSelectorMenu:
export const Default = () => (
<AssetSelectorMenu sortByName={action("sortByName")} assets={assets} />
);
Inside the storybook for AssetSelectorMenu, I'd like to test that the function prop sortByName actually visually sorts the assets by name. At the moment, it only makes sure it the function gets called, but visually it's not sorting the assets. How can I do that?
If you want to use state in your Storybook examples so that your components are fully working based on interaction you need to use the createElement function from React.
Here is a simple example using a Checkbox component that has it's value managed by state which simulates using a state manager like Redux or Context etc.
import { Fragment, useState, createElement } from 'react'
<Preview>
<Story name="Default">
{createElement(() => {
const [value, setValue] = useState(['Yes'])
const onChange = (event, value) => {
setValue(value)
}
return (
<Checkbox
name="checkbox"
values={[
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' }
]}
value={value}
onChange={onChange}
/>
)
})}
</Story>
</Preview>

getting setstate to cause a rerender every time in a useEffect block

I've created this codesandbox example and here is the code:
import React, { ReactNode, useState } from "react";
import { Formik, FormikConfig, FormikProps, Form, FormikErrors } from "formik";
import { useEffect } from "react";
import { scrollToValidationError } from "./scrollToValidationError";
// const isEmpty = (a: unknown): boolean =>
// typeof a === "object" && Object.keys(a).length > 0;
export type FormContainerProps<V> = {
render({
values,
errors,
invalid,
submitCount,
isSubmitting
}: {
values: V;
invalid: boolean;
errors: FormikErrors<V>;
submitCount: number;
isSubmitting: boolean;
}): ReactNode;
additionalContent?: ReactNode;
nextButtonText?: string;
} & Pick<FormikConfig<V>, "initialValues" | "validate"> &
Partial<Pick<FormikConfig<V>, "onSubmit">>;
export const FormContainer = function FormContainer<V>({
initialValues,
additionalContent,
validate,
render,
...rest
}: FormContainerProps<V>) {
const [hasValidationError, setHasValidationError] = useState(false);
useEffect(() => {
if (!hasValidationError) {
return;
}
scrollToValidationError();
}, [hasValidationError]);
return (
<>
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={async (values, { validateForm }) => {}}
>
{({
isSubmitting,
submitCount,
isValid,
errors,
values
}: FormikProps<V>) => {
const invalid = !isValid;
if (submitCount > 0 && invalid) {
setHasValidationError(true);
}
return (
<>
<div data-selector="validation-summary">Validation Summary</div>
<Form>
<div>
<div>
{render({
values,
errors,
isSubmitting,
invalid,
submitCount
})}
</div>
<div>
<button type="submit">SUBMIT</button>
</div>
</div>
</Form>
</>
);
}}
</Formik>
</>
);
};
Basically I am calling setHasValidationError(true) which breaks the dependency watcher on useEffect
useEffect(() => {
if (!hasValidationError) {
return;
}
scrollToValidationError();
setTimeout(() => setHasValidationError(false));
}, [hasValidationError]);
But if this is a form with multiple errors then I want to trigger the useEffect every time but I don't know when to reset it to false or if there is a better way.
In order to scroll to the first error field upon clicking on submit then you can do the following:
Write a custom component (eg: FocuseabelField) that renders formik field which also handles automatic scroll to element and focus on error input
Use Formik's innerRef
Just use the formik's isSubmitting and errors to handle logic for scrolling and focussing
FocuseabelField custom component
const FocuseabelField: any = props => {
const elementRef = useRef<HTMLDivElement>();
if (
props.isSubmitting &&
elementRef.current !== undefined &&
props.errors.hasOwnProperty(props.name)
) {
elementRef.current.scrollIntoView();
elementRef.current.focus();
}
return <Field {...props} innerRef={elementRef} />;
};
Usage
<FocuseabelField
errors={errors}
isSubmitting={isSubmitting}
name="name"
placeholder="enter name"
className={errors && errors.name ? "input error" : "input"}
/>
I have taken your code and have commented the stuff like scrollToValidationerror.ts, dom.ts, wait.ts, useState(hasValidationError), useEffect etc.
Simplified working copy of the code is here. I have used 2 fields to demonstrate multiple errors & auto scroll & focus to the error field:
https://codesandbox.io/s/usemachine-typescript-problems-tns0c?file=/src/components/Home/index.tsx
When the forms gets bigger it becomes complicated to manage so its good to consider outsourcing the form validation part and use libraries such as yup and maintain a validation schema and pass it on to formik.
Have a look at the formik docs for examples.
What about creating an Object with keys for each form field? That way you can maintain a specific form validation error for each input and use that Object in the useEffect second parameter, it will make sure it's triggered for each form error update
To answer your original question,
useEffect does reference check for its dependencies, so you could use Object instead of value. So something like this.
const [hasValidationError, setHasValidationError] = useState({value: false});
useEffect(() => {
if (!hasValidationError.value) {
return;
}
scrollToValidationError();
}, [hasValidationError]);
setHasValidationError({value: true});
But in regards to Formik usage, I highly recommend you follow what #gdh pointed out.
I don't understand the need of effects here.
Why don't you call the method directly instead of using hooks?
You can avoid 2 re-renders by doing that, and the component can also be stateless!
...
}: FormikProps<V>) => {
const invalid = !isValid;
if (submitCount > 0 && invalid) {
scrollToValidationError();
}
...

Resources