vazco / uniforms

A React library for building forms from any schema.
https://uniforms.tools
MIT License
1.94k stars 239 forks source link

Custom callback in onChange #1300

Closed mariusrak closed 6 months ago

mariusrak commented 7 months ago

I have complicated input (for files) and when files change in the input, I call props.onChange(newFiles). But then I need to call a callback which would have new model available.

So basically, we just need to add a callback parameter to the onChange. i.e. in https://github.com/vazco/uniforms/blob/1a6ca83e10dd30ea8838dc2bae1478ca4772ca96/packages/uniforms/src/AutoForm.tsx#L64 we would add third parameter, which would be called in this.setState.

Currently I can achieve that only by using setTimeout right after I call the props.onChange but that's error-prone.

kestarumper commented 6 months ago

Hi @mariusrak, We're currently working on uniforms v4.0, and it's not likely that we will extend the onChange functionality in v3. I'll bring up that topic in our weekly meeting, and we'll consider this in v4.0 or v4.x.

If I understand correctly, you must run the callback at the Field level. It's true that we can't pass a callback to the onChange directly. However, you can access the whole model by using useForm that returns the uniforms context. Then, you can use useEffect to run the callback with the whole model. Use the dependency array to trigger the effect when only certain fields on the model change.

function WholeModelField({ callback }) {
  const form = useForm();
  useEffect(() => {
    callback(form.model);
  // assuming there is a field named `files` at the root
  }, [form.model.files]);
  return null;
}

To make it even simpler, you can use the second argument of useField, which also provides the uniforms form context.

function FilesField(props) {
  const [field, form] = useField("files", props);
  useEffect(() => {
    props.callback(form.model);
  }, [form.model.files]);
  return <input type="file" />;
}

Another workaround at the form level if that also suits you, is by using the onChange prop on the form directly or the onChangeModel.

<AutoForm
      schema={schema}
      onChange={(key, value) => console.log("onChange", key, value)}
      onChangeModel={(model) => console.log("onChangeModel", model)}
  />

Edit restless-tree-6zwdr5

mariusrak commented 6 months ago

Unfortunately my files field is class component, not function component and therefore I cannot use hooks. I'd have to implement it in whole form and that would mean lower reusability of files field component.

kestarumper commented 6 months ago

You can always use the HOC approach.

import React, { useEffect } from "react";
import { AutoFields, AutoForm } from "uniforms-mui";
import {
  connectField,
  FieldProps,
  filterDOMProps,
  GuaranteedProps,
  HTMLFieldProps,
  UnknownObject,
  useForm,
} from "uniforms";
import { bridge as schema } from "./schema/all-fields-schema";

class _FileField extends React.Component<
  HTMLFieldProps<string, HTMLInputElement> & { model: UnknownObject }
> {
  componentDidUpdate(): void {
    console.log("componentDidUpdate", this.props.model);
  }
  render() {
    return (
      <input
        onChange={(event) => this.props.onChange(event.target.files?.[0].name)}
        multiple={false}
        type="file"
      />
    );
  }
}

function withModelHoc<P extends UnknownObject>(
  Component: React.ComponentType<P>
) {
  return (props: Omit<P, "model">) => {
    const form = useForm();
    return <Component {...(props as P)} model={form.model} />;
  };
}

const FileField = withModelHoc(connectField(_FileField, { kind: "leaf" }));

export function App() {
  return (
    <AutoForm placeholder schema={schema}>
      <FileField name="text" />
    </AutoForm>
  );
}
mariusrak commented 6 months ago

Yeah, right, I totally forgot about this option :D Thanks :)

mariusrak commented 3 months ago

Hi I recently stumbled upon a scenario where I would again use such function and it was not in a custom field but in a quite more complicated and non-reusable component where even use of useEffect was problematic, so I would really use the callback in onChange. So I vote once again for it.

kestarumper commented 3 months ago

Hi @mariusrak, could you provide more technical details? The description is too mysterious. Were you able to use useEffect in that non-reusable component?