Label conditional on value of Select

I’m trying to render a label that has a value which depends on the selection made in another control.
Basically I have the following in schema:

“designwithPrice”:{
“type”: “string”,
“title”: “Designüberarbeitung”,
“description”: “Für einen Aufpreis können Sie individuelle Modifikationen am Design machen lassen.”,
“oneOf”: [
{ “const”: “none”, “title”: “Keine Überarbeitung”, “description”:“Keine Anpassung”, “price” : 0},
{ “const”: “M”, “title”: “Kleinere Überarbeitungen”, “price” : 500},
{ “const”: “L”, “title”: “Deutliche Überarbeitungen”, “price” : 1000},
{ “const”: “XL”, “title”: “Weitgehende Überarbeitungen”, “price” : 1750}
]
},
“designPrice”:{
“type” : “string”
}

and corresponding in uischema:
{
“type”: “Group”,
“label”: “Design”,
“elements”: [
{
“type”: “Control”,
“scope”: “#/properties/designwithPrice”
}, {
“type”: “Label”,
“text”: “Keine Mehrkosten”
}
]
}

I want the Label to display the value of the price property when the designwithPrice control changes.
What I’m missing:

  • The correct way to trigger onChange inside a custom renderer. Since a renderer isnt quite the same as a react component componentWillUpdate and similar methods don’t work.

  • A way for a label element to refer to a value from the data object. (Just like controls are bound to data via scope)

I think that I’m probably messing up the approach, mixing schema and data.

[original thread by Christian Nyffenegger]

I want the Label to display the value of the price property when the designwithPrice control changes.

You can always manually access the whole JSON Forms context and look up the data yourself with useJsonForms, i.e. const jsonforms = useJsonForms(). For example the data is located in jsonforms.core.data. By using this context the component will rerender, whenever the context content changes, i.e. also whenever the contained data changes.

The correct way to trigger onChange inside a custom renderer. Since a renderer isnt quite the same as a react component componentWillUpdate and similar methods don’t work.

As mentioned above: By using the context the renderer will automatically rerender (this is also how all other renderers update, the context usage is just “hidden” away in withJsonFormsXYZProps). Also custom renderers are really just React components and nothing more.

A way for a label element to refer to a value from the data object. (Just like controls are bound to data via scope)

You can customize your uischema in whichever what you want, so if you like to you can also add a scope to your Labels. We recommend adding all customizations into an options object, but you don’t necessarily need to. Then you can just use our utility functions to resolve from the scope to the data. Bonus points when doing it in your own custom HOC. This is roughly the code you need to manually resolve a scope defined in a options object.

  const jsonforms = useJsonForms();
  const rootData = jsonforms.core.data;
  const currentPath = composeWithUi(uischema.options.scope, path);
  const scopedData = Resolve.data(rootData, currentPath);

This will take a scope placed in the options of the Label and resolve it. path and uischema come in via the props.

I think that I’m probably messing up the approach, mixing schema and data.

Maybe, I don’t know enough about your use case to give a proper opinion. Adding the custom price to the oneOf seems mostly to be fine. What exactly is designPrice? Also what is the purpose of the extra Label in the ui schema? Shouldn’t this just be a part of a custom renderer for the designwithPrice control?

[Christian Nyffenegger]

designPrice is a hidden control, which holds the price value of the selected design, since the data object only contains the selected value of designPrice. (e.g. none/“M”,“L”,“XL”)
I have a working solution right now, but it feels very hacky.

uischema:

{
    "type": "Group",
    "label": "Design",
    "elements": [
        { 
            "type": "Control", 
            "scope": "#/properties/design",
            "options": {
                "withPrice": true
            }
        }, {
            "type": "Control",
            "scope": "#/properties/designPrice",
            "rule": {
                "effect": "HIDE",
                "condition": {
                    "scope": "#/properties/trueBoolean",
                    "schema":{
                        "const": true
                    }
                }
            }
        }
    ]
},

control:

export const SelectWithPriceControl = (props: ControlProps & OwnPropsOfEnum) => {
    // access the jsonforms object to get schemas and data
    const jsonforms = useJsonForms();    
    const schema = jsonforms.core.schema;
    // determine the path of the control
    const path = props.path;
    const control = schema.properties[path];
    const oneOf = control.oneOf;
    // extend schema to include pricePath property
    type JsonSchemaWithPricePath = JsonSchema & {pricePath: string}; 
    const SchemaWithPricePath = props.schema as JsonSchemaWithPricePath;
    const pricePath = SchemaWithPricePath.pricePath;
    // loop all properties to see which one was selected, then assign the price to our hidden property
    oneOf.forEach(option => {
        if(option.const === jsonforms.core.data[path]){
            jsonforms.core.data[pricePath] = option.price;
        }
    });
    // output the hidden property inside the control
    return (
        <Grid container >
            <Grid item sm={6}>
                <MaterialOneOfEnumControl  { ...props}  />
            </Grid>
            <Grid item sm={6}>
                { jsonforms.core.data[pricePath] }
            </Grid>
        </Grid>
        );
};

[Christian Nyffenegger]

Actually, I was now able to eliminate the designPrice control, since the jsonforms.core.data object does expose all properties, as opposed to the data prop a control receives

Well, actually it doesn’t, but i can access the values from the schema as im doing above.

Hi @christian-nyffenegger, you are right, this looks very hacky. I’ll comment on the details and then give more general advice.

        "rule": {
          "effect": "HIDE",
          "condition": {
               "scope": "#/properties/trueBoolean",
               "schema":{
                   "const": true
               }
           }
        }

You don’t need a “real” boolean here. The only important part is that the schema is successfully validating, so

"scope": "#",
"schema": {}

would always be true and therefore good enough. However in general I’m not a fan of hidden fields. So whenever this is needed I would look for another way to achieve the desired functionality.

  • The schema, i.e. control is manually resolved in your renderer. However props should already contain the resolved control, i.e. props.schema.

  • jsonforms.core.data[pricePath] = option.price;. You’re not allowed to modify any value of jsonforms directly. The content of jsonforms is contained in a React Context. By modifying it directly the update mechanism of React is bypassed, leading to weird behavior. For updating the core.data you can use the dispatch in which you can send an Actions.update event.

  • The core.data object has the whole data. When it doesn’t then because of the broken update mechanism described above.

Generally speaking I would avoid a separate “whole price” field, or at least I would avoid updating it from renderers directly.

  • Strictly speaking there is no need for a “whole price” field. It’s not really an input but just a derived value, i.e. the price can be calculated from the data object in combination with the schema. So you could move the logic completely outside of JSON Forms, listen to the data changes in JSON Forms, and then update the UI with the calculated price. This is logic you’ll need to implement on the server side anyway, as you can never trust the sent price data. Of course you can also render the value within JSON Forms if you want to, for this I would add a new UI Schema element TotalPrice (without a scope, as we don’t model the price) with a custom renderer containing the price calculation logic.

  • For redundancy and UX reasons you can of course still calculate and save the value on the client and send it to the server to check whether they agree. I would do it outside of JSON Forms. However if the price shall be rendered within JSON Forms then you could use a regular control (with readonly: true) and outside of JSON Forms you still listen to the data changes and update the data with the newly calculated price (make sure to construct a new data object, otherwise JSON Forms will not update correctly). Once #1670 is implemented the calculated price update could also happen within JSON Forms.

So in summary I would avoid incrementing and decrementing the total value within the renderers but just always cleanly calculate it from the data. This can be hard coded when your schema is always the same or dynamically derived from the data + schema with price annotations.

[Christian Nyffenegger]

I’ve tried reading up on dispatch, but as far as I can tell that is a redux concept? I’m trying to code exclusively in React, because a) I know nothing about Redux b) it’s deprecated by JsonForms.

I’ve been able to use the onChange property on the JsonForms to set state. But now I need to capture a click event from a custom component which doesn’t change any data. Or is the solution for that to just change any data inside the custom component?

[Christian Nyffenegger]

(Specificially I used it to change a custom state for our App, not state in the JsonForms object)

I’ve tried reading up on dispatch, but as far as I can tell that is a redux concept? I’m trying to code exclusively in React, because a) I know nothing about Redux b) it’s deprecated by JsonForms.

Reducers with dispatch is a core feature of React. It works similar as the one of Redux and therefore also uses the same naming. We used it to get rid of Redux. So while there is no more Redux involved, there is still a dispatch available.

I’ve been able to use the onChange property on the JsonForms to set state. But now I need to capture a click event from a custom component which doesn’t change any data. Or is the solution for that to just change any data inside the custom component?

You can solve this via React context. You create a context outside of JSON Forms, consume it in your custom renderer and then call some callback which you placed in your context. Your app can then react on the callback and do whatever you want.