How do I pass custom props?

Hello,

My company has decided that we are going to use this package and I have been given the task to implement it, but I’m having great difficulty understanding it. It would be great if you guys made some more advanced examples with custom renderers.

In my case, I want to implement a radio list that has both a primary and secondary text per option. Something akin to this: https://imgur.com/sbd6EUs.

So I tried downloading the React seed, and following the custom renderer tutorial, but I for the life of me cannot figure out how to pass custom props to this renderer. Where would I put the array containing the options with the properties primaryText, secondaryText and value?

Hi @lll,

JSON Forms has two main artifacts: The JSON Schema, describing the data, and the UI Schema (optionally) describing the layout. Rendering is taken over by renderer-tester pairs: Whenever an UI Element is to be rendered, the testers will receive the UI element as well as the corresponding (sub)-JSON schema and then indicate whether they want to take over rendering that element.

The renderer then receives the same props which were used for the dispatching process and they also have access to the form wide state. Our bindings then take the props and the form-wide state and derive all the necessary props for the renderer, e.g. label, errors, visible, enabled etc.

The renderer is also given the respective schema and uischema as props.

Usually that information is enough to render everything needed. The benefit of being JSON Schema based is that most data should be handed over in a declarative way. In your case I would expect that the JSON Schema contains a oneOf construct of const values which each have a title and/or description, e.g.

{
  "type": "object",
  "properties": {
    "plan": {
      "type": "string",
      "description": "Select a plan based on the size and needs of your team.",
      "oneOf": [
        {
          "const": "basic",
          "title": "Basic",
          "description": "Perfect for individuals and small teams."
        },
        {
          "const": "pro",
          "title": "Pro",
          "description": "For growing teams and businesses."
        },
        {
          "const": "enterprise",
          "title": "Enterprise",
          "description": "For large organizations and custom solutions."
        }
      ]
    }
  },
}

In this case you can access all of this information in the schema prop handed over to your custom renderer for plan, no custom props are needed.


In case you can’t adapt the schemas used and you actually need to inject some custom props into your renderer, then you need to use React contexts. With them you can inject arbitrary information and callbacks into your renderers from outside of JSON Forms.

Alternatively, if your schemas are more simple, for example like this:

{
  "type": "object",
  "properties": {
    "plan": {
      "type": "string",
      "enum": ["basic", "pro", "enterprise"],
    }
  },
}

Then you could either add all the additional text in your UI Schema element, or you use the i18n support of JSON Forms to derive the additional labels in your custom renderer.

Hello Stefan

Thank you for your reply!

I tried something similar to what you’ve suggested earlier and thought I was doing it wrong. I now implemented what you did, and this is the console-log from my custom “control”: https://imgur.com/c1heEWN

I only see the “oneOf” props deeply nested in schema > properties > myRadioObject > oneOf.
Surely these should be on the top level along with data, description, errors, label, path and such, no?

Something does not fit on your side. You have a UI Schema with a scope #/properties/myRadioObject/radioList but this does not exist in your schema. That’s probably why your schema is also not properly resolved. The UI Schema element should only point to #/properties/myRadioObject.

Can you post your full schema, uischema and custom renderer code? It’s difficult to help properly without that information.

Sure. Here’s the schema:


const schema1 = {
  type: "object",
  properties: {
    myRadioObject: {
      type: "string",
      oneOf: [
        {
          const: "1",
          title: "First title",
          description: "First description",
        },
        {
          const: "2",
          title: "Second title",
        },
      ],
    },
  },
};

here is the ui schema:


const uiSchema1 = {
  type: "VerticalLayout",
  elements: [
    {
      type: "Control",
      scope: "#/properties/myRadioObject/radioList",
      label: "myRadioObject",
    },
  ],
};

and here is the control I am logging from:

const RadioListControl = (props: Props) => {
  console.log("all props", props);
  return (
    <RadioList
      onSelect={(newValue) => props.handleChange(props.path, newValue)}
      {...props}
    />
  );
};

export default withJsonFormsOneOfProps(RadioListControl);

here’s the radio list:


const RadioList = (props: RadioListProps) => {
  console.log("props from the actual radio list", props);
  return <h1 id="#/properties/radioList">Hey from Radio List</h1>;
};

(This console logs the same as the control)

Hi @lll,

As mentioned above, the scope in the UI schema should look like this:

const uiSchema1 = {
  type: "VerticalLayout",
  elements: [
    {
      type: "Control",
      scope: "#/properties/myRadioObject",
      label: "myRadioObject",
    },
  ],
};

The scope in a control needs to be resolvable against the schema.


In the renderer you are using withJsonFormsOneOfProps which is a very generic binding for oneOfs. In your case you can use the more specialized withJsonFormsOneOfEnumProps which already resolve the options for you. If you want to do it fully “manually” then just use the withJsonFormsControlProps.

I’m not understanding this scoping part. If I remove the reference to “radioList”, and use the scope you suggest, then it doesn’t render my custom control anymore, but a default input?

this is my tester:

export default rankWith(
  scopeEndsWith("radioList"),
);

I assume I can’t name the scope “#/properties/radioList” if I want to be able to reusse this component multiple times?

Hi @lll,

The scope should not be used as a free-form field. At least our bindings assume that it contains a valid pointer into the schema and use it accordingly. So I would recommend to not encode additional information in there. If you add additional information there, you would need to copy/adapt a larger part of our bindings and the gain is minuscule compared to the effort.

Instead you should write a “smarter” tester. Our RankedTester look like this:

(uischema, schema, context) => number

The rankWith and scopeEndsWith etc. are just helpers we provide to more easily create such a tester.

In case you want to reuse a component multiple times, then you usually not use the scopeEndsWith helper as you then need to name all your properties the same. Instead you can check a specfic schema structure or uischema property, and then react accordingly.

Let’s say you want to save a rendering-hint in the JSON Schema, for example like this:

const schema1 = {
  type: "object",
  properties: {
    myRadioObject: {
      type: "string",
      x-rendering-hint: "radioList",
      oneOf: [
        {
          const: "1",
          title: "First title",
          description: "First description",
        },
        {
          const: "2",
          title: "Second title",
        },
      ],
    },
  },
};

Then your tester could look like this:

rankWith(10, and(isOneOfEnumControl, schemaMatches(schema => schema.x-rendering-hint === 'radioList')));

See here for all our exported helpers.

WIth some modifications to your last reply I seem to finally have made it work. Thank you SO much for your help! I truly appreciate it.