Updating Formik State whilst using Headless UI "Listbox" (which manages its own state, internally) - reactjs

I'm struggling to get a Headless UI Listbox implementation working successfully with Formik. After reading all the docs and scouring StackOverflow, I'm yet to find an answer.
I can get the Listbox to function perfectly well on its own, and Formik is working with other, less complex (but still custom) components. That said, I can't get them working in unison. Whenever I change the selection in the Select component, I can successfully update the Headless UI state (in as much as it updates to the correct value) and the Formik state, but for some reason the active and selected properties (within Headless UI) aren't working correctly (always false).
I assume what I need to do is to make use of the onChange handler so that it updates both the Headless UI state (to keep the active and selected properties updated) and the Formik value, but this doesn't seem to work as expected.
Does anyone have any suggestions? Please see a minimal representation of the code below:
export function TestForm() {
return (
<Formik
initialValues={{ testing: {} }}
onSubmit={ alert("Submitted"); }
>
<Form>
<Select
name="testing"
items={[
{ id: 1, name: "Test 1" },
{ id: 2, name: "Test 2" },
{ id: 3, name: "Test 3" },
]}
/>
</Form>
)
}
export function Select(props) {
// Get field properties
const [field, meta, helpers] = useField(props);
// Set initial value for `Select`
const [selectedItem, setSelectedItem] = useState(null);
// On change, update `Headless UI` and `Formik` values
const handleChange = (newValue) => {
setSelectedItem(newValue); // Update the Headless UI state
helpers.setValue(newValue); // This seems to "break" the Headless UI state
};
// Return `Select` structure
return (
<Listbox
value={selectedItem}
name={props.name}
onChange={handleChange}
// onBlur={field.onBlur} ...Commented out for now as trying to figure out issue with `onChange`
>
<Listbox.Label>Test Label:</Listbox.Label>
<Listbox.Button>
{selectedItem ? selectedItem.name : "-- Select --"}
</Listbox.Button>
<Listbox.Options>
{props.items.map((item) => {
return (
<Listbox.Option
className={({ active }) => (active ? "active" : "")}
key={item.id}
value={item}
>
{({ selected }) => (
<>
{item.name}
{selected ? <CheckIcon /> : null}
</>
)}
</Listbox.Option>
);
})}
</Listbox.Options>
</Listbox>
);
}

I managed to make it work like this:
import { useField } from "formik";
interface SelectProps {
name: string;
label: string;
options: string[];
}
export const Select: React.FC<SelectProps> = ({
name,
label,
options,
}) => {
const [field] = useField({ name });
return (
<Listbox
value={field.value}
onChange={(value: string) => {
field.onChange({ target: { value, name } });
}}
>
...
</Listbox>
);
};

Related

Material UI's Autocomplete does not show the selected value in React using react-hook-form

I am having issues making Material UI's Autocomplete show the selected item using react-hook-form with options that are objects containing name and id. When I select an item, the box is just empty.
I have a FieldArray of custom MaterialBars which contain a material:
<Controller
control={control}
name={`materialBars.${index}.materialId`}
render={(
{ field: { value, onChange } }
) => (
<Autocomplete
options={materials.map(material => ({id: material.id, name: material.name})} // materials are fetched from the API
getOptionLabel={(option) => option.name}
value={materialItems.find((item) => `${item.id}` === value) || null}
onChange={(_, val) => onChange(val?.id)}
renderInput={(params) => <TextField {...params} label="Material" />}
/>
)}
/>
There is a working example in the codesandbox.
When I select a material in the list, the box is cleared, and the placeholder text is shown instead of the selected material (when focus is removed from the box). The item is selected under the hood, because in my application, when I press submit, the newly selected material is saved to the backend.
I cannot figure out if the issue lies in the react-hook-form part, material UI or me trying to connect the two. I guess it will be easier if the options are just an array of strings with the name of the material (when the form schema has just the materialId), but it is nice to keep track of the id, for when contacting the API.
You should set the same type on materialId property between FormValue and ListData.
For Example, if I use number type, it should be
https://codesandbox.io/s/autocomplete-forked-mpivv1?file=/src/MaterialBar.tsx
// App.tsx
const { control, reset } = useForm<FormValues>({
defaultValues: {
materialBars: [
// use number instead of string
{ ..., materialId: 1 },
{ ..., materialId: 6 }
]
}
});
// util.ts
export const materials = [
{
id: 1, // keep it as number type
...
},
...
];
// util.ts
export type FormValues = {
materialBars: {
// change it from string type to number
materialId: number;
...
}[];
};
// MaterialBar.tsx
<Controller
...
) => (
<Autocomplete
...
/*
Remove curly brackets
- change`${item.id}` to item.id
*/
value={materialItems.find((item) => item.id === value) || null}
/>
)}
/>

How does a parent know about the state of a list of child forms using formik?

I have a form consisting of a list of building materials which can be materials already registered for the construction and new materials:
The MaterialBar:
function MaterialBar({
materialBarId,
material,
onDelete,
onChange,
}: MaterialBarProps) {
const { values, handleChange } = useFormik<MaterialBar>({
initialValues: material,
onSubmit: console.log,
enableReinitialize: true,
});
const updateField = (event) => {
handleChange(event);
onChange(materialBarId, values);
};
return (
<DropdownWrapper>
<Dropdown
label="Material"
items={/* A list of available materials */}
selectedItem={values.material}
onSelect={updateField}
/>
.... Other fields
<TextField
name="amount"
id="material-amount"
label="Amount"
type="number"
onChange={updateField}
/>
<DeleteButton onClick={() => onDelete(materialBarId)}>
<img src={remove} />
</DeleteButton>
</DropdownWrapper>
);
}
The parent (ConstructionMaterials):
function ConstructionMaterials({
project,
materials,
attachMaterial,
}: ProjectMaterialsProps) {
const [allMaterials, setAllMaterials] = useState<IProjectMaterial[]>(
getProjectMaterialsFrom(project.materials)
);
const saveAllMaterials = () => {
allMaterials.forEach((newMaterial) => {
if (newMaterial.isNew) {
attachMaterial(newMaterial);
}
});
};
const updateNewMaterial = (
materialBarId: number,
updatedMaterial: MaterialBar
): void => {
const updatedList: IProjectMaterial[] = allMaterials.map((material) => {
if (material.projectMaterialId === materialBarId) {
return {
...material,
materialId: materials.find(
(currentMaterial) =>
currentMaterial.name === updatedMaterial.material
)?.id,
type: updatedMaterial.type,
length: updatedMaterial.length,
width: updatedMaterial.width,
value: updatedMaterial.amount,
};
}
return material;
});
setAllMaterials(updatedList);
};
// Adds a new empty material to the list
const addMaterial = (): void => {
setAllMaterials([
...allMaterials,
{
projectMaterialId: calculateMaterialBarId(),
projectId: project.id,
isNew: true,
},
]);
};
return (
<>
{allMaterials.map((material) => {
const materialBar: MaterialBar = {
material: material.name || "",
type: material.type || "",
amount: material.value || 0,
length: material.length || 0,
width: material.width || 0,
};
return (
<AddMaterialBar
key={material.projectMaterialId}
materialBarId={material.projectMaterialId!}
materials={materials}
onDelete={removeMaterial}
onChange={updateNewMaterial}
/>
);
})}
<Button onClick={() => saveAllMaterials()}>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</>
);
}
I have a hard time figuring out how to manage the list of materials. I use Formik (the useFormik hook) in the MaterialBar to take care of the values of each field.
My challenge is how to keep all the data clean and easily pass it between the components while knowing which materials are new and which already exist. If I just use Formik in the MaterialBar, then ConstructionMaterials does not know about the changes made in each field and it needs the updated data because it calls the backend with a "save all" action (the "Save"-buttons in the image should not be there, but they are my temporary fix).
To circumvent this, I also keep track of each material in ConstructionMaterials with the onChange on MaterialBar, but that seems redundant, since this is what Formik should take care of. I have also added a isNew field to the material type to keep track of whether it is new, so I don't two lists for existing and new materials.
I have had a look at FieldArray in ConstructionMaterials, but shouldn't Formik be used in the child, since the parent should just grab the data and send it to the API?
So: is there a clever way to handle a list of items where the parent can know about the changes made in the childs form, to make a bulk create request without the parent having to also keep track of all the objects in the children?
Sorry about the long post, but I don't know how to make it shorter without loosing the context.

how to refresh the antd pro ProFormText initialValue

I am using antd pro to develop an app, now facing a problem is that the ProFormText initialValue did not update when the props changed. I pass the record from props and give it to the ModalForm, this is the code looks like:
const UpdateForm: React.FC<UpdateFormProps> = (props) => {
const intl = useIntl();
const { initialState } = useModel('##initialState');
return (
<ModalForm
title={intl.formatMessage({
id: 'pages.apps.jobs.interview.updateInterview',
defaultMessage: 'New rule',
})}
width="400px"
visible={props.updateModalVisible}
onVisibleChange={(value)=>{
if(!value){
props.onCancel();
}
}}
onFinish={props.onSubmit}
>
<ProFormText
initialValue={props.values.company}
name="company"
label={intl.formatMessage({
id: 'pages.apps.jobs.interview.searchTable.company',
defaultMessage: 'company',
})}
width="md"
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.updateForm.ruleName.nameRules"
defaultMessage="Please input the nameļ¼"
/>
),
},
]}
/>
);
}
when open the modal and give the initial value, the next time when the props value change, the ProFormText still keep the first time value. I have read this question: Update antd form if initialValue is changed seems it only works on antd. the ModalForm did not contain the useForm() method. what should I do to fix this problem and keep the value changed follow props? This is the version info:
"#ant-design/pro-form": "^1.52.0",
"#ant-design/pro-layout": "^6.32.0",
"#ant-design/pro-table": "^2.61.0",
I am facing the same problem with you, and tried follow the Update antd form if initialValue is changed instructions and works. First add:
const [form] = Form.useForm()
and bind the form with ModalForm like this:
<ModalForm
form = {form}
title={intl.formatMessage({
id: 'pages.apps.jobs.interview.updateInterview',
defaultMessage: 'New rule',
})}
width="400px"
visible={props.updateModalVisible}
onVisibleChange={(value)=>{
if(!value){
props.onCancel();
}
}}
onFinish={props.onSubmit}
>
you may facing the error Module "./antd/es/form/style" does not exist in container, just delete the .umi cache folder and rebuild the project. Finally add this code to reset the fields when the props changed:
useEffect(() => {
form.resetFields();
form.setFieldsValue(props.values);
});

(React-redux) BlueprintJs Suggest cannot backspace in input after making a selection

class MySuggest extends React.Component<Props, State> {
....
....
private handleClick = (item: string, event: SyntheticEvent<HTMLElement, Event>) => {
event.stopPropagation();
event.preventDefault();
this.props.onChange(item);
}
public render() {
const { loading, value, error} = this.props;
const { selectValue } = this.state;
const loadingIcon = loading ? <Icon icon='repeat'></Icon> : undefined;
let errorClass = error? 'error' : '';
const inputProps: Partial<IInputGroupProps> = {
type: 'search',
leftIcon: 'search',
placeholder: 'Enter at least 2 characters to search...',
rightElement: loadingIcon,
value: selectValue,
};
return (
<FormGroup>
<Suggest
disabled={false}
onItemSelect={this.handleClick}
inputProps={inputProps}
items={value}
fill={true}
inputValueRenderer={(item) => item.toString()}
openOnKeyDown={true}
noResults={'no results'}
onQueryChange={(query, event) => {
if (!event) {
this.props.fetchByUserInput(query.toUpperCase());
}
}}
scrollToActiveItem
itemRenderer={(item, { modifiers, handleClick }) => (
<MenuItem
active={modifiers.active}
onClick={() => this.handleClick(item) }
text={item}
key={item}
/>
)}
/>
</FormGroup>
);
}
}
Everything works fine, I am able to make a selection from drop-down list, however I cannot use backspace in input if I made a selection. I checked the official documentation(https://blueprintjs.com/docs/#select/suggest), it has the same issue in its example. Does anyone has the similar problems and solutions?
The reason for this is once you type something in the field, it becomes an element of the page, so once you make a selection, it assumes you highlighted an element, so will assume you are trying to send the page a command for that selection (backspace is the default page-back command for most browsers).
Solution:
Create a new dialog input every time the user makes a selection, so the user can continue to make selections and edits.
It took forever.. post my solution here:
be careful about two things:
1. query = {.....} needed to control the state of the input box
2. openOnKeyDown flag, it makes the delete not working

React Redux - select all checkbox

I have been searching on Google all day to try and find a way to solve my issue.
I've created a "product selection page" and I'm trying to add a "select all" checkbox that will select any number of products that are displayed (this will vary depending on the customer).
It's coming along and I've got all the checkboxes working but I can't get "select all" to work. Admittedly I'm using some in-house libraries and I think that's what's giving me trouble as I'm unable to find examples that look like what I've done so far.
OK, so the code to create my checkboxGroup is here:
let productSelectionList = (
<FormGroup className="productInfo">
<Field
component={CheckboxGroup}
name="checkboxField"
vertical={true}
choices={this.createProductList()}
onChange={this.handleCheckboxClick}
helpText="Select all that apply."
label="Which accounts should use this new mailing address?"
/>
</FormGroup>
);
As you can see, my choices will be created in the createProductList method. That looks like this:
createProductList() {
const { products } = this.props;
const selectAllCheckbox = <b>Select All Accounts</b>;
let productList = [];
productList.push({ label: selectAllCheckbox, value: "selectAll" });
if (products && products.length > 0) {
products.forEach((product, idx) => {
productList.push({
label: product.productDescription,
value: product.displayArrangementId
});
});
}
return productList;
}
Also note that here I've also created the "Select All Accounts" entry and then pushed it onto the list with a value of "selectAll". The actual products are then pushed on, each having a label and a value (although only the label is displayed. The end result looks like this:
Select Products checkboxes
I've managed to isolate the "select all" checkbox with this function:
handleCheckboxClick(event) {
// var items = this.state.items.slice();
if (event.selectAll) {
this.setState({
'isSelectAllClicked': true
});
} else {
this.setState({
'isSelectAllClicked': false
});
}
}
I also created this componentDidUpdate function:
componentDidUpdate(prevProps, prevState) {
if (this.state.isSelectAllClicked !== prevState.isSelectAllClicked && this.state.isSelectAllClicked){
console.log("if this ", this.state.isSelectAllClicked);
console.log("if this ", this.props);
} else if (this.state.isSelectAllClicked !== prevState.isSelectAllClicked && !this.state.isSelectAllClicked){
console.log("else this ", this.state.isSelectAllClicked);
console.log("else this ", this.props);
}
}
So in the console, I'm able to see that when the "select all" checkbox is clicked, I do get a "True" flag, and unclicking it I get a "False". But now how can I select the remaining boxes (I will admit that I am EXTREMELY new to React/Redux and that I don't have any previous checkboxes experience).
In Chrome, I'm able to see my this.props as shown here..
this.props
You can see that this.props.productList.values.checkboxField shows the values of true for the "select all" checkbox as well as for four of the products. But that's because I manually checked off those four products for this test member that has 14 products. How can I get "check all" to select all 14 products?
Did I go about this the wrong way? (please tell me that this is still doable) :(
My guess is your single product checkboxes are bound to some data you have in state, whether local or redux. The checkbox input type has a checked prop which accepts a boolean value which will determine if the checkbox is checked or not.
The idea would be to set all items checked prop (whatever you are actually using for that value) to true upon clicking the select all checkbox. Here is example code you can try and run.
import React, { Component } from 'react';
import './App.css';
class App extends Component {
state = {
items: [
{
label: "first",
checked: false,
},
{
label: "last",
checked: false,
}
],
selectAll: false,
}
renderCheckbooks = (item) => {
return (
<div key={item.label}>
<span>{item.label}</span>
<input type="checkbox" checked={item.checked} />
</div>
);
}
selectAll = (e) => {
if (this.state.selectAll) {
this.setState({ selectAll: false }, () => {
let items = [...this.state.items];
items = items.map(item => {
return {
...item,
checked: false,
}
})
this.setState({ items })
});
} else {
this.setState({ selectAll: true }, () => {
let items = [...this.state.items];
items = items.map(item => {
return {
...item,
checked: true,
}
})
this.setState({ items })
});
}
}
render() {
return (
<div className="App">
{this.state.items.map(this.renderCheckbooks)}
<span>Select all</span>
<input onClick={this.selectAll} type="checkbox" checked={this.state.selectAll} />
</div>
);
}
}
export default App;
I have items in state. Each item has a checked prop which I pass to the checkbox getting rendered for that item. If the prop is true, the checkbox will be checked otherwise it wont be. When I click on select all, I map thru my items to make each one checked so that the checkbox gets checked.
Here is a link to a codesandbox where you can see this in action and mess with the code.
There is a package grouped-checkboxes which can solve this problem.
In your case you could map over your products like this:
import React from 'react';
import {
CheckboxGroup,
AllCheckerCheckbox,
Checkbox
} from "#createnl/grouped-checkboxes";
const App = (props) => {
const { products } = props
return (
<CheckboxGroup onChange={console.log}>
<label>
<AllCheckerCheckbox />
Select all accounts
</label>
{products.map(product => (
<label>
<Checkbox id={product.value} />
{product.label}
</label>
))}
</CheckboxGroup>
)
}
More examples see https://codesandbox.io/s/grouped-checkboxes-v5sww

Resources