flipbyte / formik-json-schema

Easily create react forms using JSON / Javascript Object
MIT License
151 stars 40 forks source link

auto-saving forms #19

Closed andrewringler closed 5 years ago

andrewringler commented 5 years ago

I am trying to auto-save the Form I am generating with formik-json-schema. I have instructions for how to do this with just formik, Auto-saving forms #172 and more specifically I am following the gist here, but I am not sure how to adapt this to work with formik-json-schema.

With this approach a new component is created called <AutoSave> which is rendered as a child of <Form>. I am not sure with formik-json-schema however how I would modify the render or the children of Form to add the <AutoSave> component. Is there a way to append arbitrary children to the form rendered by formik-json-schema? Or is there an approach to implementing auto-save that might make more sense?

Thanks,

Andrew

easeq commented 5 years ago

Using version 0.2.2, you can create a custom container renderer for the Autosave component as follows:

import _ from 'lodash'
import React from 'react';
import { Element } from '@flipbyte/formik-json';

class Autosave extends React.Component {
    state = {
       isSaving: false,
    }

    componentWillReceiveProps(nextProps, nextContext) {
       if (!_.isEqual(nextProps.formik.values, this.props.formik.values)) {
          this.save()
       }
    }

    save = _.debounce(() => {
        this.setState({ isSaving: true, saveError: undefined })
        this.props.config.onSave(this.props.formik.values)
          .then(
            () => this.setState({ isSaving: false, lastSaved: new Date() }),
            () => this.setState({ isSaving: false, saveError })
          )
    }, 300);

    render() {
        const { name, elements } = this.props.config;
        return (
            <React.Fragment>
                { _.map(elements, ( element, key ) => (
                    <Element key={ key } config={ element } />
                )) }
            </React.Fragment>
        );
    }
}

export default Autosave;

Your form schema object should look something like this:

{
    ...your top level form renderer,
    elements: {
        autosave: {
            id: 'autosave',
            type: 'container',
            renderer: Autosave,
            onSave: values => new Promise((resolve) => {
                resolve('saved');
            }),
            elements: {
                ...your elements
            }
        }
    }
}
andrewringler commented 5 years ago

Thank you for the extremely quick response and for rolling out a new release. Yes, this solution works well for me.

Here is the final AutoSave.js I ended up using:

import React from 'react';
import { Element } from '@flipbyte/formik-json/lib';
import _ from 'lodash';

// https://github.com/flipbyte/formik-json-schema/issues/19
class AutoSave extends React.Component {
  state = {
     isSaving: false,
     saveError: null
  }

  componentWillReceiveProps(nextProps, nextContext) {
    if (!_.isEqual(nextProps.formik.values, this.props.formik.values)) {
      this.save()
    }
  }

  save = _.debounce(() => {
    this.setState({ isSaving: true, saveError: undefined })
    this.props.config.onSave(this.props.formik.values)
    .then(() => this.setState({ isSaving: false, lastSaved: new Date() }))
    .catch((saveError) => this.setState({ isSaving: false, saveError: saveError }))
  }, this.props.debounce)

  render() {
    const { elements } = this.props.config;
    return (
        <React.Fragment>
            { _.map(elements, ( element, key ) => (
                <Element key={ key } config={ element } />
            )) }
        </React.Fragment>
    );
  }
}

export default AutoSave;

My form component looks something like the following. Note that i'm using the formik-json-schema registry so that I can reference AutoSave by name, since I am actually pulling the form specification in as JSON from a server API. I am also binding my save function to the form spec in componentDidMount. I didn't show render, but its similar to the formik-json-schema examples.

import React, { Component } from 'react';
import { Form, registry } from '@flipbyte/formik-json';
import AutoSave from './AutoSave';
const DEBOUNCE_TIME_MS = 300;

class Survey extends Component {
  constructor(props) {
    super(props);
    registry.registerContainer('autosave', AutoSave);
  }

  componentDidMount() {
    this.props.surveySpec.elements.autosave.onSave = values => this.putDataToDatabasePromise(values);
    this.props.surveySpec.elements.autosave.debounce = DEBOUNCE_TIME_MS;
  }

  putDataToDatabasePromise(values) {
    return axio.put …
  }

  …
}

export default Survey;

And my form JSON string looks something like:

{
  id: "survey1",
  label: "Survey 1",
  type: "container",
  renderer: "form",
  elements: {
    question1: {
      type: "field",
      renderer: 'textarea',
      name: "question1",
      label: "Question 1",
      formGroupClass: "group",
    },
    submit: {
      type: "field",
      renderer: "button",
      name: "submit",
      label: "Submit",
      buttonType: "submit"
    },

    autosave: {
      id: 'autosave',
      type: 'container',
      debounce: 300,
      renderer: 'autosave',
      onSave: () => console.error('you must set onSave function in parent component')
    }
  }
}