plantain-00 / schema-based-json-editor

A reactjs and vuejs component of schema based json editor.
MIT License
168 stars 38 forks source link

JSONEditor component not re-rendering on change of props #36

Open rosesyrett opened 2 years ago

rosesyrett commented 2 years ago

Version: 8.3.0

Environment: Using create-react-app with typescript, packaged with npm.

Code:

I have a simple react application which has a 'user' page and an 'admin' page. For now this is not attached to any API calls, instead I have an array of objects stored inside 'templates', each object is a 'template' with information about fields required, their datatypes, any restrictions etc. The user page should have an interface where the user can select which collection to use, and a form which renders based on this selection. It also has a submit button to add their entry somewhere - this is still in development.

Below is the snippet of code relating to this issue.

import { JSONEditor, ObjectSchema, Schema, ValueType } from "react-schema-based-json-editor";
type JSXNode = JSX.Element | null;
interface field {
    "name": string,
    "description": string,
    "type": string,
    "value": string | number,
    "units": string,
    "options": string[],
    "validation": string
}

interface collection {
    "id": number,
    "version": string,
    "name": string,
    "beamline": string,
    "owner": string,
    "fields": field[]
}

templates: collection[] = [
    {
        "id": 1,
        "version": "v1.0.0",
        "name": "Ptychography",
        "beamline": "I14",
        "owner": "Testy McTestface",
        "fields": [
            {
                "name": "composition",
                "description": "Element composition of your sample",
                "type": "string",
                "value": "", // the default value to be used.
                "units": "",
                "options": []
            },
            {
                "name": "weight",
                "description": "Weight, in grams, of your sample",
                "type": "number",
                "value": 20, // the default value to be used.
                "units": "g",
                "options": []
            },
            {
                "name": "motor",
                "description": "The motor to be used - one of two.",
                "type": "string",
                "value": "", // the default value to be used.
                "units": "",
                "options": [
                    "motor1",
                    "motor2"
                ]
            }
        ]
    },
    {
        "id": 2,
        "version": "v1.0.0",
        "name": "Crystallography",
        "beamline": "B07",
        "owner": "Hakuna Matata",
        "fields": [
            {
                "name": "composition",
                "description": "Element composition of your sample",
                "type": "string",
                "value": "",
                "units": "",
                "options": []
            },
            {
                "name": "state",
                "description": "The state of matter to be used.",
                "type": "string",
                "value": "",
                "units": "",
                "options": []
            },
            {
                "name": "motor",
                "description": "The motor to be used - one of two.",
                "type": "string",
                "value": "motor1",
                "units": "",
                "options": [
                    "motor1",
                    "motor2"
                ]
            }
        ]
    }
}

// to be passed as input to JSONEditor element for schema.
function generateSchema(coll: collection | undefined): ObjectSchema | undefined {
    const schemaProperties: { [name: string]: Schema } | undefined = coll ?
        coll.fields.reduce<{ [name: string]: Schema }>((result, field) => {
            const schema: Schema = {
                type: field.type as "string",
                description: field.description,
                default: field.value,
                enum: field.options.length > 0 ? field.options : undefined,
            }
            return {
                [field.name]: schema,
                ...result
            };
        }, {}) : undefined;

    if (schemaProperties && coll) {
        const generatedSchema: ObjectSchema = {
            title: coll.name,
            type: "object",
            properties: schemaProperties,
            required: Object.keys(schemaProperties),
            collapsed: false
        };
        return generatedSchema;
    }
    return undefined;
}

// to be passed as input to JSONEditor element for InitialValue.
function generateInitialVals(schema: ObjectSchema | undefined): ValueType | undefined {
    if (schema) {
        const initialVals = Object.entries(schema.properties).reduce<{ [name: string]: ValueType }>(
            (result, pair) => {
                const [k, v] = pair;
                result[k] = v.default;
                return result;
            }, {}
        )
        return initialVals;
    }
    return undefined;
}

function MakeForm(props: { useCollection: collection | undefined }): JSXNode {
    const coll: collection | undefined = props.useCollection;
    const collSchema = generateSchema(coll);
    const initialValue = generateInitialVals(collSchema);

    const [values, setValues] = useState<ValueType>(initialValue)

    if (!coll)
        return (
            <h1>Select a valid template</h1>
        )

    console.log("schema is: ", collSchema);

    const updateValue = (value: any, isValid: boolean) => {
        if (isValid) setValues({ ...value });
    };
    return (
        <div>
            <JSONEditor schema={collSchema as ObjectSchema} updateValue={updateValue} initialValue={initialValue} v-if="visible" theme="bootstrap5" />
        </div>
    );
}

function UserPage() {
    const collections = [...templates.map(item => item.name)];
    const [coll, setColl] = useState<string>("");

    return (
        <div className="App">
            <header className="App-header">Welcome, User!</header>
            <body className="App-body">
                <SelectCollection collections={collections} coll={coll} setColl={setColl} />
                <MakeForm useCollection={templates.find(c => c.name === coll)} />
                <SubmitButton />
            </body>
        </div>
    );
}

function App() {
  return (
    <div>
      <NavigationBar />
      <main>
        <Routes>
          <Route path="/" element={<h1>Home Page</h1>} /> 
          <Route path="/admin" element={<AdminPage />} />
          <Route path="/user" element={<UserPage />} />
        </Routes>
      </main>
    </div>
  )
}

In reality, the code from above is spread across multiple files, but is included here for completeness in one chunk. Finally, the index.tsx looks like the following:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'bootstrap/dist/css/bootstrap.css';
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

reportWebVitals();

Expected:

When running npm start and navigating to /user, I expect to see a dropdown button (from my SelectCollection function component) which lets me select a template by name, i.e. 'Ptychography' or 'Crystallography'. When this is selected, the MakeForm component recieves a prop which is the actual collection corresponding to that name. It then should re-render and create a form based on a generated schema using this template. If I select another collection name from the dropdown which exists in the templates array such as 'Crystallography', I would expect the MakeForm component to re-render, and the resulting form from JSONEditor to change to reflect the changed schema.

Actual:

Once I select 'Ptychography', the form entries do not change if I then select 'Crystallography'. Instead, any form elements which are required only by 'Ptychography' now have a 'not exists' checkbox next to them. It seems as though the schema does not change, when in fact I can verify the schema is correctly being generated with console logging. Only if I refresh the page and select 'Crystallography' does the correct form show up.

It seems as though JSONEditor is just not re-rendering. A useEffect hook hasnt helped with this either.