rekit / antd-form-builder

Dynamic meta driven React forms based on antd.

Home Page:https://rekit.github.io/antd-form-builder

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

getFieldValue not working with custom component

colonder opened this issue · comments

I'm writing a page for creating an opinion about a particular thing and I'd like it to be a multi-step form with coordinated Select components, each querying different REST API endpoint. I tried to combine several things: antd's Search Box as well as antd-form-builder's Wizard. However, from what I can understand from the source code of antd-form-builder, onSearch is not supported in the select components so I had to make my own custom one that would be able to query REST API. Currently, I have the following code (this is essentially a copy-paste of the Wizard example linked above with a custom search box component from antd website linked above):

import React, {useState, useCallback} from "react";
import FormBuilder from "antd-form-builder";
import {Form, Button, Steps, Select} from 'antd'
import querystring from "querystring";
import axios from "axios";
import {api} from "../api";

const {Option} = Select;
const {Step} = Steps

let timeout;
let currentValue;

function fetch(url, value, query, callback) {
    if (timeout) {
        clearTimeout(timeout);
        timeout = null;
    }
    currentValue = value;

    async function getData() {
        if (currentValue === value) {
            const str = querystring.encode({
                name: value,
                q: query,
            });
            const res = await axios.get(`${url}?${str}`);
            callback(res.data);
        }
    }

    timeout = setTimeout(getData, 500);
}

const SearchInput = ({url, query, ...props}) => {
    const [data, setData] = useState([]);
    const [value, setValue] = useState(props.initialValue);

    const handleSearch = value => {
        if (value) {
            console.log(query)
            fetch(url, value, query, result => {
                setData(result);
            });
        } else {
            setData([]);
        }
    };

    const handleChange = value => {
        setValue(value)
    };

    return (
        <Select
            showSearch
            value={value}
            defaultValue={props.defaultValue? props.defaultValue : undefined}
            disabled={props.disabled}
            showArrow={true}
            style={{fontSize: "large"}}
            filterOption={false}
            onSearch={handleSearch}
            onChange={handleChange}
        >
            {data.map(d => <Option style={{fontSize: "large"}} key={d.id} value={d.name}>{d.name}</Option>)}
        </Select>
    );
}

FormBuilder.defineWidget('search-input', SearchInput)

const OpinionsCreate = () => {
    const [form] = Form.useForm()
    const [currentStep, setCurrentStep] = useState(0)
    const forceUpdate = FormBuilder.useForceUpdate()
    const handleFinish = useCallback(() => {
        console.log('Submit: ', form.getFieldsValue(true))
    }, [form])

    const wizardMeta = {
        steps: [
            {
                title: 'Strona wysyłająca',
                formMeta: {
                    columns: 1,
                    fields: [
                        {
                            key: 'sending_university',
                            required: true,
                            disabled: true,
                            widget: "search-input",
                            widgetProps: {
                                initialValue: 'Some university',
                                url: api.endpoint1,
                                defaultValue: "Some university",
                                onChange: () => {
                                    // Clear sending_faculty value when country is changed
                                    form.setFieldsValue({sending_faculty: undefined})
                                },
                            }
                        },
                        {
                            key: 'sending_faculty',
                            required: true,
                            widget: "search-input",
                            widgetProps: {
                                url: api.endpoint2,
                                query: form.getFieldValue("sending_university"),
                            }
                        },
                    ],
                },
            },
        ]
    }

    // Clone the meta for dynamic change
    const newWizardMeta = JSON.parse(JSON.stringify(wizardMeta))
    // In a wizard, every field should be preserved when swtich steps.
    newWizardMeta.steps.forEach(s => s.formMeta.fields.forEach(f => (f.preserve = true)))

    // Generate a general review step
    const reviewFields = []
    newWizardMeta.steps.forEach((s, i) => {
        reviewFields.push(
            {
                key: 'review' + i,
                colSpan: 2,
                render() {
                    return (
                        <fieldset>
                            <legend>{s.title}</legend>
                        </fieldset>
                    )
                },
            },
            ...s.formMeta.fields,
        )
    })

    newWizardMeta.steps.push({
        key: 'review',
        title: 'Podsumowanie',
        formMeta: {
            columns: 2,
            fields: reviewFields,
        },
    })

    const stepsLength = newWizardMeta.steps.length

    const handleNext = () => {
        form.validateFields().then(() => {
            setCurrentStep(currentStep + 1)
        })
    }
    const handleBack = () => {
        form.validateFields().then(() => {
            setCurrentStep(currentStep - 1)
        })
    }
    const isReview = currentStep === stepsLength - 1

    return (
        <Form
            layout="horizontal"
            form={form}
            onValuesChange={forceUpdate}
            style={{width: '100%'}}
            onFinish={handleFinish}
        >
            <Steps current={currentStep}>
                {newWizardMeta.steps.map(s => (
                    <Step key={s.title} title={s.title}/>
                ))}
            </Steps>
            <div style={{background: '#f7f7f7', padding: '20px', margin: '30px 0'}}>
                <FormBuilder
                    viewMode={currentStep === stepsLength - 1}
                    form={form}
                    meta={newWizardMeta.steps[currentStep].formMeta}
                />
            </div>
            <Form.Item className="form-footer" style={{textAlign: 'right'}}>
                {currentStep > 0 && (
                    <Button onClick={handleBack} style={{float: 'left', marginTop: '5px'}}>
                        Back
                    </Button>
                )}
                <Button>Cancel</Button>&nbsp; &nbsp;
                <Button type="primary" onClick={isReview ? () => form.submit() : handleNext}>
                    {isReview ? 'Submit' : 'Next'}
                </Button>
            </Form.Item>
        </Form>
    )
}

export default OpinionsCreate;

There are several things going on here

  1. the first select has a default value, and I'd like this select to be disabled such that this default value couldn't be changed (I don't exactly know how I can achieve this, but I tried)
  2. the second select (in theory) should access the value in the first select, and query the backend endpoint api.endpoint2, which is a URL, with an additional ?q=... parameter, such that only certain options would be returned (the second select is coordinated)
  3. In the SearchInput there is a handleSearch method that fires whenever the user types something in the text input. The backend is queried such that the value from the first select is taken into account as well as the text typed by the user is contained in the retrieved options.

The custom search component is working alright itself, however, I stumbled upon several issues:

  1. I can't get the value from the first select by using form.getFieldValue("sending_university") so the second select queries just entire table in the database which is not what I wanted. It somehow doesn't recognize that there's an initial value in the first component.
  2. Even when I add a third select component, dependent on the second one, it doesn't recognize the value selected in the second component either.
  3. When I try to continue to the next step, validation fails, claiming that all fields' values are empty, even the first one with the default initial value.

I tried many combinations all to no avail. I'm pretty sure, what I want is doable but I'm already short of ideas. Does anyone see what's the issue and can help me out?

A custom component in antd's form should have value and onChange props. It's not related with antd form builder. Take a look at: https://rekit.github.io/antd-form-builder/examples-v4/#custom-component .

OK, and how can I implement this onSearch method that takes value from another select?

I don't want a third-party component, I want to use the default Select component with onSearch method.

The title of the issue indicates it's a bug of form builder but it's totally about the usage of antd's form api. Please ask such questions on stackoverflow rather than raising an issue.