Render nested forms in flat structure

Hi everyone,
This is follow up discussion of add a deep ui schema generator by weiner · Pull Request #1666 · eclipsesource/jsonforms · GitHub

  • Summary
    In the process of developing custom renderers for Gutenberg Components, I want to integrate Navigator Component because I want to display nested props on separate views and only use single schema

To render the Navigator Component markup] requires the form screens in on a flat structure, therefore current jsonforms renders the nested props in nested markup.

The original question was about the need of a uischema generator to get all the form screens from root node, but then I decided to have a new context to store the screens content ( which is JsonFormDispatch ) WP-Builder: Feat/nested Navigator context by bangank36 · Pull Request #27 · bangank36/WP-Builder · GitHub

  • ObjectRenderer update its screen via context and only display the NavigatorButton
  • The new NavigatorLayout renderer receives the screen content from nested props and render its list of NavigatorScreen

chrome-capture-2023-6-11

Though this approach works well as you can see in the attached recording above, as it is compatible with jsonforms current features set such as show/hide rules, you may see the obvious issue here with Context usage: Provider and Consumer unnecessary re-rendering

useEffect(() => {
    // Use the callback since the new state is based on the previous state
    setScreenContent(prevScreenContent => ({
      ...prevScreenContent,
      [route]: {
        component: (<JsonFormsDispatch
          visible={visible}
          enabled={enabled}
          schema={schema}
          uischema={detailUiSchema}
          path={path}
          renderers={renderers}
          cells={cells}
        />),
        label: detailUiSchema.label,
        path: path
      }
    }))
  }, [route])

I hope you can help me to improve the context usage to optimize the app and if there is built-in feature that can help achieve the similar requirements (render nested object in flat structure), I am happy to know

cc: @sdirix
Thank you!

Hi @bangank36,

The original question was about the need of a uischema generator to get all the form screens from root node,
[…]
if there is built-in feature that can help achieve the similar requirements (render nested object in flat structure)

The nested rendering happens because:

  • Every nested object is rendered by the object renderer
  • The object renderer generates a UI Schema for the object and dispatches again
  • The generated UI Schema is a Group which is why it looks nested

An easy way to render flat lists therefore is to overwrite the generic Group renderer with an own implementation and simply let it generate a VerticalLayout instead of a Group. In the DOM the controls will still be nested in layout divs, however by default the VerticalLayout has no margin nor padding so it is not visible.

As an alternative you could of course adapt the UI Schema generation more deeply and instead of rendering nested objects via an object control, look further into them and generate controls for the nested properties directly.

I hope you can help me to improve the context usage to optimize the app

The used approach can’t be optimized as it rerenders by design, i.e. the parent component is informed about new elements during rendering of one of its children. This will always rerender no matter what you do. With the usage of React.memo parts of it could be improved, but the overall issue will remain.

The clean solution is to get rid off the context and determine beforehand which screens you want to show. As the screens are aligned with the use of objects this can be analyzed beforehand in a straightforward manner. Did you try the approach which I suggested here?

Yes, thank you for suggested, please find the detail report here with recording POC: Update navigator layout by bangank36 · Pull Request #36 · bangank36/WP-Builder · GitHub
This is good approach, but it will only works with 01 level of nested data, if we have more level, then the 2nd level will display on the same screen as shown in the screenshot

Actually I am concern about the nested DOM nodes, while the NavigatorProvider of Gutenberg requires all the screens to be on the same level of the main screen, that why I am looking for effective way so that all the deep nested screens can be rendered on the root node, which requires a deep generated UISchema or context. ( again, am I missing a built-in feature that can do that? )
I am aware context makes providers and consumers re-rendered by design, but I will try to improve performance using useMemo somehow
Attached is the recording of what have been implemented
chrome-capture-2023-6-11

Thanks for helping

Hi @bangank36,

You still don’t need deeply generated UI schemas for the deeply nested cases. You just need to hand over the parameters to the JSON Dispatch accordingly.

Should look something like this. Note that I wrote this down without testing. Code can obviously be optimized to not join the path array all the time, etc.

/**
* collects all (nested) screens in the contained schema and returns appropriate rendering information
* expects and returns path in array format
*/
const analyzeSchemaForScreens = (schema, path) => {
  const result = [];
  Object.entries(schema.properties ?? {}).forEach( ([key, value]) => {
    if(value.type === 'object') {
      result.push({ schema, uischema: control(`#/properties/${encode(key)}`), path };
      result.push(...analyzeSchemaForScreens(value, [...path, key]));
    }
  });
  return result;
}

const NavigatorLayoutRenderer = (props) => {
  const screenInformation = useMemo(() => analyzeSchemaForScreens(props.schema, props.path.split('.'), [props.schema, props.path]);
  return (
  <NavigatorProvider initialPath="/">
      <NavigatorScreen path="/">
        <p>{{props.label}}</p>
        {{screenInformation.map(screenEntry => (
          <NavigatorButton path={`/${screenEntry.path.join('/')}`} key={screenEntry.path.join()}>
             Navigate to {{screenEntry.path.join('/')}}.
          </NavigatorButton>
        )}}
      </NavigatorScreen>
      {{screenInformation.map(screenEntry => (
        <NavigatorScreen path={`/${screenEntry.path.join('/')}`} key={screenEntry.path.join()}>
           <JsonFormsDispatch schema={screenEntry.schema} uischema={screenEntry.uischema} path={screenEntry.path.join('.')}/>
        </NavigatorScreen>
      )}}
  </NavigatorProvider>);
}

const control = (scope) => ({
  type: 'Control',
  scope
});

Edit: Note that for the objects contained within you want to have a custom object renderer which replaces nested object rendering with NavigatorButtons. You can then either determine the NavigatorButton’s path by converting your props, or you make the screenInformation available via a context from the NavigatorLayoutRenderer. This is fine here as the context is created once and not modified on the fly during rendering.

Yeah I agreed, this is also my approach to not lean on the deep nested uischema, as described I let the nested props generate its own uischema and pass to root via context

Agreed (ref)

I am moving on with the Array renderer since it need to iterate over the data to render the items