I need to react to the next click in the material ui stepper. I want to slide up to the top of the form on nexthandler

Due to some long forms users want to have next and previous click to go to the top of the #stepper form. Need some tips on how to make this happen. I guess a customrender, but since a stepper with navbuttons already exists i need some pointers:)

Hi @trillian74,

Yes, a custom renderer is the way to go. The stepper renderer is quite simplistic so I would like to suggest to copy it and modify it your needs. You can find it here.

Thanks for the suggestion. I’ll do a copy of the code and transform it to my needs.

Ole

I tried creating a second version in my source, but the form disappears after the final rendering when i use it. Do i have to create my own “version” of the material renderers package?

Ole

Hi @sdirix

I tried creating a second version in my source, but the form disappears after the final rendering when i use it. Do i have to create my own “version” of the material renderers package?

Ole

Hi @trillian74,

You need to create a custom renderer which is added to renderer registry (i.e. an array of tester/renderer pairs). There is no need to build an own version of the Material renderers.

You can see how this is done in the tutorial or the React seed.

thanxs @sdirix - i’ve done some custom renderes with success. This one though, gives me problems. The way i see it is that i can copy that file into my code - exactly as it is and set another rank number and this one should be selected instead. when i add it to my renderes… eventhough i have materials renderes in my form?


const renderers = [
  ...materialRenderers,
  //register custom renderers
  {
    tester: materialCategorizationStepperTester, // this one is mine (but equal)
    renderer: MaterialCategorizationStepperLayoutRenderer,// this one is mine (but equalish... have rank number 3 instead of 2)
  },
  {
    tester: contactPickerManagementControlTester,
    renderer: ContactPickerManagementControl,
  },
  {
    tester: peoplePickerManagementControlTester,
    renderer: PeoplePickerManagementControl,
  },
  {
    tester: singleChipSelectControlTester,
    renderer: SingleChipSelectSingleControl,
  },
  { tester: multiChipSelectControlTester, renderer: MultiChipSelectControl },
];

so when i apply the uischema it just dissapears from the gui… without an error message.
Any suggestions?

@sdirix i found out why in the end. I don’t properly understand why it differs, but the property visible was undefined on the contrary to the one in material renderers.

public render() {
    const {
      data,
      path,
      renderers,
      schema,
      uischema,
      visible = true, // i just hardcoded this - but i would like it not to be
      cells,
      config,
      ajv,
    } = this.props;
    const categorization = uischema as Categorization;
    const activeCategory = this.state.activeCategory;
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
    const buttonWrapperStyle = {
      textAlign: "right" as "right",
      width: "100%",
      margin: "1em auto",
    };
    const buttonNextStyle = {
      float: "right" as "right",
    };
    const buttonStyle = {
      marginRight: "1em",
    };
    const childProps: MaterialLayoutRendererProps = {
      elements: categorization.elements[activeCategory].elements,
      schema,
      path,
      direction: "column",
      visible,
      renderers,
      cells,
    };

Not sure how this can happen. Our bindings should make sure that visible is set. Can you debug to find the cause of the issue? Are @jsonforms/core, @jsonforms/react and @jsonforms/material-renderers on the exact same version?

@jsonforms/core”: “2.5.2”,
@jsonforms/material-renderers”: “2.5.2”,
@jsonforms/react”: “2.5.2”,

@sdirix Find my code below only edit is that the next button is named Next123 and rank is 3

The stepper from the package works alright, but this one makes it dissapear when it enter

 <div>
          <MaterialLayoutRenderer {...childProps} />
        </div>
import * as React from "react";
import merge from "lodash/merge";
import { Button, Hidden, Step, StepButton, Stepper } from "@material-ui/core";
import {
  and,
  Categorization,
  categorizationHasCategory,
  Category,
  isVisible,
  optionIs,
  RankedTester,
  rankWith,
  StatePropsOfLayout,
  uiTypeIs,
} from "@jsonforms/core";
import { RendererComponent, withJsonFormsLayoutProps } from "@jsonforms/react";
import {
  AjvProps,
  MaterialLayoutRenderer,
  MaterialLayoutRendererProps,
  withAjvProps,
} from "@jsonforms/material-renderers";

export const myMaterialCategorizationStepperTester: RankedTester = rankWith(
  3,
  and(
    uiTypeIs("Categorization"),
    categorizationHasCategory,
    optionIs("variant", "stepper")
  )
);

export interface CategorizationStepperState {
  activeCategory: number;
}

export interface MaterialCategorizationStepperLayoutRendererProps
  extends StatePropsOfLayout,
    AjvProps {
  data: any;
}

export class MyMaterialCategorizationStepperLayoutRenderer extends RendererComponent<
  MaterialCategorizationStepperLayoutRendererProps,
  CategorizationStepperState
> {
  constructor(props: MaterialCategorizationStepperLayoutRendererProps) {
    super(props);
    this.state = { activeCategory: 0 };
  }

  public handleStep = (step: number) => {
    this.setState({ activeCategory: step });
  }

  public render() {
    const {
      data,
      path,
      renderers,
      schema,
      uischema,
      visible,
      cells,
      config,
      ajv,
    } = this.props;
    const categorization = uischema as Categorization;
    const activeCategory = this.state.activeCategory;
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
    const buttonWrapperStyle = {
      textAlign: "right" as "right",
      width: "100%",
      margin: "1em auto",
    };
    const buttonNextStyle = {
      float: "right" as "right",
    };
    const buttonStyle = {
      marginRight: "1em",
    };
    const childProps: MaterialLayoutRendererProps = {
      elements: categorization.elements[activeCategory].elements,
      schema,
      path,
      direction: "column",
      visible,
      renderers,
      cells,
    };
    console.log(childProps);
    const categories = categorization.elements.filter((category: Category) =>
      isVisible(category, data, undefined, ajv)
    );
    return (
      <Hidden xsUp={!visible}>
        <Stepper activeStep={activeCategory} nonLinear>
          {categories.map((e: Category, idx: number) => (
            <Step key={e.label}>
              <StepButton onClick={() => this.handleStep(idx)}>
                {e.label}
              </StepButton>
            </Step>
          ))}
        </Stepper>
        <div>
          <MaterialLayoutRenderer {...childProps} />
        </div>
        {!!appliedUiSchemaOptions.showNavButtons ? (
          <div style={buttonWrapperStyle}>
            <Button
              style={buttonNextStyle}
              variant="contained"
              color="primary"
              disabled={activeCategory >= categories.length - 1}
              onClick={() => this.handleStep(activeCategory + 1)}
            >
              Next123
            </Button>
            <Button
              style={buttonStyle}
              color="secondary"
              variant="contained"
              disabled={activeCategory <= 0}
              onClick={() => this.handleStep(activeCategory - 1)}
            >
              Previous
            </Button>
          </div>
        ) : (
          <></>
        )}
      </Hidden>
    );
  }
}

export default withJsonFormsLayoutProps(
  withAjvProps(MyMaterialCategorizationStepperLayoutRenderer)
);

Are you actually registering the wrapped default export or did you by accident register the raw renderer? That would explain why the props are not properly determined. Did you debug the withJsonFormsLayoutProps binding? Is it called?

@sdirix thanx for your patience with me:)
I’m not sure what you mean about register the raw renderer. Do you mean that i import from is wrong and that i should create my own?

import {
  AjvProps,
  MaterialLayoutRenderer,
  MaterialLayoutRendererProps,
  withAjvProps,
} from "@jsonforms/material-renderers";

Hi @trillian74,

I meant the way how your custom control is registered. Can you show where you add the renderer and the tester to the renderers registry and how you import your custom renderer?

@sdirix Hi again. You are so right. i had

 {
    tester: myMaterialCategorizationStepperTester,
    renderer: MyMaterialCategorizationStepperLayoutRenderer,  // this was my bad
  },

changed it to

 {
    tester: myMaterialCategorizationStepperTester,
    renderer: MyMaterialCategorizationStepperLayout,
  },

man, it was right in front of me. thank you for your patience.

1 Like

Hi
I am trying to create the custom stepper and using the above example and getting some errors, could you please let me know what i was doing wrong.

import * as React from 'react';
import merge from 'lodash/merge';
import { Button, Hidden, Step, StepButton, Stepper } from '@material-ui/core';
import {
  and,
  Categorization,
  categorizationHasCategory,
  Category,
  isVisible,
  optionIs,
  RankedTester,
  rankWith,
  StatePropsOfLayout,
  uiTypeIs,
} from '@jsonforms/core';
import { RendererComponent, withJsonFormsLayoutProps } from '@jsonforms/react';
import {
  AjvProps,
  MaterialLayoutRenderer,
  MaterialLayoutRendererProps,
  withAjvProps,
} from '@jsonforms/material-renderers';

export const myMaterialCategorizationStepperTester: RankedTester = rankWith(
  3,
  and(
    uiTypeIs('Categorization'),
    categorizationHasCategory,
    optionIs('variant', 'stepper')
  )
);

export interface CategorizationStepperState {
  activeCategory: number;
}

export interface MaterialCategorizationStepperLayoutRendererProps
  extends StatePropsOfLayout,
    AjvProps {
  data: any;
}

export class MyMaterialCategorizationStepperLayoutRenderer extends RendererComponent<
  MaterialCategorizationStepperLayoutRendererProps,
  CategorizationStepperState
> {
  constructor(props: MaterialCategorizationStepperLayoutRendererProps) {
    super(props);
    this.state = { activeCategory: 0 };
  }

  public handleStep = (step: number) => {
    this.setState({ activeCategory: step });
  };

  public render() {
    const {
      data,
      path,
      renderers,
      schema,
      uischema,
      visible,
      cells,
      config,
      ajv,
    } = this.props;
    const categorization = uischema as Categorization;
    const activeCategory = this.state.activeCategory;
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
    const buttonWrapperStyle = {
      textAlign: 'right' as 'right',
      width: '100%',
      margin: '1em auto',
    };
    const buttonNextStyle = {
      float: 'right' as 'right',
    };
    const buttonStyle = {
      marginRight: '1em',
    };
    const childProps: MaterialLayoutRendererProps = {
      elements: categorization.elements[activeCategory].elements,
      schema,
      path,
      direction: 'column',
      visible,
      renderers,
      cells,
    };
    console.log(childProps);
    const categories = categorization.elements.filter(
      (category: Category ***| Categorization***) =>
        isVisible(category, data, ***''***, ajv)
    );
    return (
      <Hidden xsUp={!visible}>
        <Stepper activeStep={activeCategory} nonLinear>
          {categories.map((e: Category *| Categorization*, idx: number) => (
            <Step key={e.label}>
              <StepButton onClick={() => this.handleStep(idx)}>
                {e.label}
              </StepButton>
            </Step>
          ))}
        </Stepper>
        <div>
          <MaterialLayoutRenderer {...childProps} />
        </div>
        {!!appliedUiSchemaOptions.showNavButtons ? (
          <div style={buttonWrapperStyle}>
            <Button
              style={buttonNextStyle}
              variant='contained'
              color='primary'
              disabled={activeCategory >= categories.length - 1}
              onClick={() => this.handleStep(activeCategory + 1)}
            >
              Next123
            </Button>
            <Button
              style={buttonStyle}
              color='secondary'
              variant='contained'
              disabled={activeCategory <= 0}
              onClick={() => this.handleStep(activeCategory - 1)}
            >
              Previous
            </Button>
          </div>
        ) : (
          <></>
        )}
      </Hidden>
    );
  }
}

export default withJsonFormsLayoutProps(
  withAjvProps(MyMaterialCategorizationStepperLayoutRenderer)
);

got error at few places Type ‘Categorization’ is not assignable to type ‘Category’.
TS2769

90 |     };
91 |     console.log(childProps);

92 | const categories = categorization.elements.filter((category: Category) =>
| ^
93 | isVisible(category, data, undefined, ajv)
94 | );
95 | return (
Changed line 92 to (category: Category | Categorization) and next line removed undefined and passing empty string in i isVisible(category, data, ‘’, ajv). Changed the line 99 to {categories.map((e: Category | Categorization, idx: number) => (

now getting an error
Argument of type ‘(props: MaterialCategorizationStepperLayoutRendererProps) => Element’ is not assignable to parameter of type ‘ComponentType’.
Type ‘(props: MaterialCategorizationStepperLayoutRendererProps) => Element’ is not assignable to type ‘FunctionComponent’.
Types of parameters ‘props’ and ‘props’ are incompatible.
Property ‘ajv’ is missing in type ‘PropsWithChildren’ but required in type ‘MaterialCategorizationStepperLayoutRendererProps’. TS2345

138 |
139 | export default withJsonFormsLayoutProps(

140 | withAjvProps(MyMaterialCategorizationStepperLayoutRenderer)
| ^
141 | );
142 |

Any help is appreciated. Thanks in advance.

Hi @vivek, this is rather hard to follow. Can you post your code on a branch or in an executable demonstrator so I can easily reproduce this on my side?

@sdirix Hi,

I have working example code example.

Visible value is getting undefined, updated the visible value as following
let updatedProps = { …childProps };
updatedProps.visible = !updatedProps.visible;
childProps = { …updatedProps };

to show up the elements changed the line 102

Not sure what i am doing is right and also i am trying enable the “NEXT” button if current step is valid. i am getting “ajv” value is undefined. Could you please let me now when you get a chance.

Thanks in advance
Vivek

Hi @vivek, by coincidence your code contains the same error as discussed with the previous person in the thread. Your renderer is wrapped and default exported (!!) in lines 148-150. However in App.tsx you are not importing the default exported renderer but the raw version.

So please change your import to

import MyRendererNameWhichCanBeAnything, {
  myMaterialCategorizationStepperTester
} from "./MyMatCatLayoutRenderer";

and register that renderer instead.

Not sure what i am doing is right and also i am trying enable the “NEXT” button if current step is valid. i am getting “ajv” value is undefined. Could you please let me now when you get a chance.

Only enabling the “next” button when the current data is valid is not that easy as the controls can point to arbitrary parts of the data. Of course if all your steps are always contained in separate objects you can take some large shortcuts.

What you have to do is to check all current errors, filter them according to your current ui schema and check whether some are left. To access all errors you can use

const ctx = useJsonForms();
const errors = ctx.core.errors;

To filter errors according to scope you can check out mapStateToControlProps and how it’s done there.


As I said at some point before: This use case is usually better handled by implementing an own stepper outside of JSON Forms and rendering each separate step via JSON Forms instead of placing all of this in a single form.

Thank you so much for quick response. Filtering errors should work. Thanks

1 Like