react-jsonschema-form icon indicating copy to clipboard operation
react-jsonschema-form copied to clipboard

Asynchronously fetch options (or dynamic select options in general)

Open dtaub opened this issue 7 years ago • 12 comments

I couldn't find an issue for this or anything in the docs, but is there any way to have a field asynchronously fetch the options for a select dropdown?

I'm sure actually making http requests would be outside the scope of this library but is there any way to do something along the lines of giving a field a promise that resolves to an array of options? Or even synchronously passing a dynamic list of options that are not known when creating the schema?

dtaub avatar Jan 11 '18 20:01 dtaub

I don't think that is possible to do right now.

You can already change the form or schema in real time, to do that, just update the schema/uiSchema and rerender the component, all of the form values will be kept

edi9999 avatar Jan 13 '18 15:01 edi9999

Has there been any progress on this?

lmeerwood avatar Apr 21 '20 23:04 lmeerwood

Im also interested to know if there has any been progress in this area?

shannonjohnstone avatar Apr 23 '20 21:04 shannonjohnstone

select drown options loading from an external source is pretty common for any UI application, is this support available yet as this issue was created long back?

hiitskiran avatar Jul 13 '20 15:07 hiitskiran

I am also interested. Now, I have to explore other libraries.

gagandeep avatar Aug 05 '20 18:08 gagandeep

This hasn't been built into the core library at the moment (though would welcome a PR), but you can do this with a custom field. Here's an example:

https://github.com/epicfaace/CFF/blob/9add15f2ce49480c10ecae508ddb920f6e64b7a2/scripts/src/form/form_widgets/AutoPopulateField.tsx

http://docs.chinmayamission.com/cff/uischema/fields/auto-populate/

epicfaace avatar Aug 05 '20 19:08 epicfaace

This hasn't been built into the core library at the moment (though would welcome a PR), but you can do this with a custom field. Here's an example:

https://github.com/epicfaace/CFF/blob/9add15f2ce49480c10ecae508ddb920f6e64b7a2/scripts/src/form/form_widgets/AutoPopulateField.tsx

http://docs.chinmayamission.com/cff/uischema/fields/auto-populate/

Anyone has a sandbox example of this? :cat:

jordan-aye avatar Sep 11 '20 17:09 jordan-aye

This hasn't been built into the core library at the moment (though would welcome a PR), but you can do this with a custom field. Here's an example:

https://github.com/epicfaace/CFF/blob/9add15f2ce49480c10ecae508ddb920f6e64b7a2/scripts/src/form/form_widgets/AutoPopulateField.tsx

http://docs.chinmayamission.com/cff/uischema/fields/auto-populate/

@epicfaace It seems like there's a heap of functionality in the Chinmayamission/CFF repo that would be really really useful in react-jsonschema-form core. Is there a good reason why you haven't pulled it in given you're involved in both projects?

GFoley83 avatar Mar 10 '22 02:03 GFoley83

It takes time to do that. Feel free to do it if you'd like though.

epicfaace avatar Mar 15 '22 14:03 epicfaace

import { Select } from "antd";
import axios from "axios";
import { WidgetProps } from "@rjsf/utils";
import { useId } from "@mantine/hooks";
import { queryParams } from "../../../api";
import { useEffectAsync } from "../../../common/utils";

interface SelectOptions {
  value: string;
  label: string;
}

const { Option } = Select;

const AsyncSelectWidget: React.FC<
  WidgetProps & {
    uiSchema: {
      "ui:options": {
        api: (queryParams?: queryParams | undefined) => string;
        searchKey: string;
        labelKey: string;
        valueKey: string;
      };
    };
  }
> = (props) => {
  const handleChange = (selectedValue: any) => {
    props.onChange(selectedValue);
  };

  const { api, searchKey, labelKey, valueKey } = props.uiSchema["ui:options"];
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState<SelectOptions[]>([]);

  const fetchData = async (search: { [key: string]: any }) => {
    try {
      setLoading(true);
      const response = await axios.post(api({ page: 1, page_size: 5 }), search);
      const data: SelectOptions[] = response.data.results.map((item: any) => ({
        label: item[labelKey],
        value: item[valueKey],
      }));
      setLoading(false);
      return data;
    } catch (error) {
      setLoading(false);
      console.error("Error fetching data:", error);
      return [];
    }
  };

  const handleSearch = async (searchTerm: string) => {
    const data = await fetchData({
      [labelKey]: {
        $regex: searchTerm,
        $options: "i",
      },
    });
    setOptions(data);
  };

  useEffectAsync(async () => {
    const data = await fetchData({});
    setOptions(data);
  }, []);

  return (
    <div style={{ width: "100%" }}>
      <div>
        {props.schema.title}
        {props.schema.required ? "*" : null}
      </div>
      <Select
        loading={loading}
        id={props.id}
        value={props.value}
        disabled={props.disabled || props.readonly}
        allowClear={!props.required}
        onChange={handleChange}
        onSearch={handleSearch}
        showSearch
        optionFilterProp="children" // Enables search by option label
        style={{ width: "100%" }}
        defaultValue={props?.uiSchema?.["ui:options"]?.defaultValue}
      >
        {options?.map((option: SelectOptions, index) => (
          <Option key={index.toString()} value={option.value}>
            {option.label}
          </Option>
        ))}
      </Select>
    </div>
  );
};

export { AsyncSelectWidget };

``` working on this, will update the whole widget once done, i use antd for the select, you can use whatever you want,

b33lz3bubTH avatar Jan 31 '24 12:01 b33lz3bubTH

import React, { useState, useEffect, useRef } from "react";
import { Select } from "antd";
import axios from "axios";
import { WidgetProps } from "@rjsf/utils";
import { queryParams } from "../../../api";
import { useEffectAsync } from "../../../common/utils";

interface SelectOptions {
  value: string;
  label: string;
}

const { Option } = Select;

const AsyncSelectWidget: React.FC<
  WidgetProps & {
    uiSchema: {
      "ui:options": {
        api: (queryParams?: queryParams | undefined) => string;
        searchKey: string;
        labelKey: string;
        valueKey: string;
      };
    };
  }
> = (props) => {
  const handleChange = (selectedValue: any) => {
    props.onChange(selectedValue);
  };

  const { api, searchKey, labelKey, valueKey } = props.uiSchema["ui:options"];
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState<SelectOptions[]>([]);
  const [page, setPage] = useState(1);
  const [total, setTotal] = useState(0);
  const scrollRef = useRef<HTMLDivElement | null>(null);

  const fetchData = async (
    search: { [key: string]: any },
    nextPage: number
  ) => {
    try {
      setLoading(true);
      const response = await axios.post(
        api({ page: nextPage, page_size: 3 }),
        search
      );
      const data: SelectOptions[] = response.data.results.map((item: any) => ({
        label: item[labelKey],
        value: item[valueKey],
      }));
      setLoading(false);
      setTotal(response.data.total);
      return data;
    } catch (error) {
      setLoading(false);
      console.error("Error fetching data:", error);
      return [];
    }
  };

  const handleSearch = async (searchTerm: string) => {
    const data = await fetchData(
      {
        [labelKey]: {
          $regex: searchTerm,
          $options: "i",
        },
      },
      1
    );
    setOptions(data);
    setPage(1);
  };

  const handleScroll = async () => {
    const scrollElement = scrollRef.current;
    if (scrollElement) {
      const { scrollTop, clientHeight, scrollHeight } = scrollElement;
      if (scrollTop + clientHeight === scrollHeight && options.length < total) {
        const nextPage = page + 1;
        const newData = await fetchData({}, nextPage);
        setOptions((prevOptions) => [...prevOptions, ...newData]);
        setPage(nextPage);
      }
    }
  };

  useEffectAsync(async () => {
    // console.log(`props value, for select`, props.formData);
    let default_data: SelectOptions[] = [];
    if (props.formData) {
      const data = await fetchData(
        {
          [searchKey]: props.formData,
        },
        1
      );
      default_data = data;
    }
    const data = await fetchData({}, 1);
    setOptions([...data, ...default_data]);
    setPage(1);
  }, []);

  return (
    <div style={{ width: "100%" }}>
      <div>
        {props.schema.title}
        {props.schema.required ? "*" : null}
      </div>
      <Select
        loading={loading}
        id={props.id}
        value={props.value}
        disabled={props.disabled || props.readonly}
        allowClear={!props.required}
        onChange={handleChange}
        onSearch={handleSearch}
        showSearch
        optionFilterProp="children" // Enables search by option label
        style={{ width: "100%" }}
        defaultValue={props.formData}
        onPopupScroll={handleScroll}
        ref={scrollRef}
      >
        {options?.map((option: SelectOptions, index) => (
          <Option key={index.toString()} value={option.value}>
            {option.label}
          </Option>
        ))}
      </Select>
    </div>
  );
};

export { AsyncSelectWidget };

This is the final code that i am gonna use in my lib, working like charm.


const uiSchema: UiSchema = {
category_id: {
      "ui:field": "AsyncSelectWidget",
      "ui:options": {
        api: Api.CategoriesList,
        searchKey: "category_id",
        labelKey: "name",
        valueKey: "category_id",
      },
    },
}

This is how am using it

b33lz3bubTH avatar Jan 31 '24 12:01 b33lz3bubTH

// Function to focus the select element const openSelect = () => { if (selectRef.current) { selectRef.current.showPicker(); } };

ahmad118128 avatar Aug 05 '24 15:08 ahmad118128