Custom material-ui renderer for react-input-mask

I’d like to use the react-input-mask component for input fields such as phone number, which will constrain the user’s input to a pre-defined pattern, and fill in the “fixed” characters in the pattern automatically. A good example of using react-input-mask in a material-ui input field can be seen here: React Text Field component - Material UI.

What is the proper way to achieve this via a JSON Forms custom renderer? I know that I can create a custom component and a control, and create a tester so that my control is used when I want it to be used. The challenge I’m facing is exactly how to write my control so that it uses my custom component, to override the “standard” behavior which involves a MaterialTextControl that renders a MaterialInputControl.

As explained in the example that I linked above, the component that I create to render the masked input needs to accept an inputRef property, as shown below. But I’m not sure how to get a hold of the inputRef within the context of the JSON Forms components and properties, so that I can pass it from my custom control class into my custom component.

function TextMaskCustom(props) {
  const { inputRef, ...other } = props;

  return (
    <MaskedInput
      {...other}
      ref={(ref) => {
        inputRef(ref ? ref.inputElement : null);
      }}
      mask={['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]}
      placeholderChar={'\u2000'}
      showMask
    />
  );
}

[original thread by Kevin Ilsen]

[Kevin Ilsen]

I was able to get it to work by overriding the MaterialInputControl component, and then using my own wrapper component for the Input component (because MaterialInputControl doesn’t expose a way to pass through a different inputComponent property for the Input component).

I’m sharing the code below in case anyone else finds it useful (or cares to suggest an improvement). The tester checks for the presence of a “mask” option and then that option is used to set the mask (so this can be used for phone numbers, social security numbers, postal codes, etc. – anything that can be specified as an array of regular expressions). I also allow the placeholder character to be specified as an option.

import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import MaskedInput from 'react-text-mask';
import {Input} from '@material-ui/core';
import {useTheme} from '@material-ui/core/styles';
import {
  RankedTester,
  ControlProps,
  schemaMatches,
  rankWith,
  isStringControl,
  and
} from '@jsonforms/core';
import {withJsonFormsControlProps} from '@jsonforms/react';
import {MaterialInputControl} from '@jsonforms/material-renderers/lib/controls/MaterialInputControl';

const MaskedInputWrapper = props => {
  const {inputRef, mask, placeholderChar, ...other} = props;
  return (
      <MaskedInput
          ref={(ref) => {
            inputRef(ref ? ref.inputElement : null);
          }}
          mask={mask}
          placeholderChar={placeholderChar}
          showMask={true}
          {...other}
      />
  );
};

MaskedInputWrapper.propTypes = {
  inputRef: PropTypes.func.isRequired
};

const InputWrapper = props => {
  const {
    data,
    config,
    className,
    id,
    enabled,
    uischema,
    isValid,
    path,
    handleChange,
    schema,
    muiInputProps
  } = props;
  const maxLength = schema.maxLength;
  const appliedUiSchemaOptions = _.merge({}, config, uischema.options);
  let inputProps: any;
  if (appliedUiSchemaOptions.restrict) {
    inputProps = {maxLength: maxLength};
  } else {
    inputProps = {};
  }

  inputProps = _.merge(inputProps, muiInputProps);

  if (appliedUiSchemaOptions.trim && maxLength !== undefined) {
    inputProps.size = maxLength;
  }

  // Input props for the MaskedInput component.
  // Convert each character spec in the mask to a RegExp (unless it's a single character
  inputProps.mask = schema.options.mask.map(characterSpec => characterSpec.length === 1 ? characterSpec : new RegExp(characterSpec));
  // If no placeholder character specified, default to unicode 2000 (EN space)
  inputProps.placeholderChar = schema.options.placeholderChar || '\u2000';

  const onChange = (ev: any) => handleChange(path, ev.target.value);

  const theme: JsonFormsTheme = useTheme();
  const inputDeleteBackgroundColor = theme.jsonforms?.input?.delete?.background || theme.palette.background.default;

  return (
      <Input
          type={
            appliedUiSchemaOptions.format === 'password' ? 'password' : 'text'
          }
          value={data || ''}
          onChange={onChange}
          className={className}
          id={id}
          disabled={!enabled}
          autoFocus={appliedUiSchemaOptions.focus}
          multiline={appliedUiSchemaOptions.multi}
          fullWidth={!appliedUiSchemaOptions.trim || maxLength === undefined}
          inputProps={inputProps}
          error={!isValid}
          inputComponent={MaskedInputWrapper}
      />
  )
};


export const maskedInputControl = (props: ControlProps) => {
  const {input, ...other} = props;
  return (
      <MaterialInputControl {...other} input={InputWrapper}/>
  );
};

export default withJsonFormsControlProps(maskedInputControl);

export const maskedInputControlTester: RankedTester = (
    rankWith(
        100,
        and(
            isStringControl,
            schemaMatches(schema => schema.hasOwnProperty('options') && !!schema.options.mask)
        )
    )
);

Hi! Great that you found a solution! Architecture wise this looks exactly as I would have recommended.

I think it makes sense to also allow customizing inputComponent. Therefore and because you had to import MaterialInputControl from the lib path I quickly created a PR to fix these issues #1700. Would this be enough for you to reuse MuiInputText?

You don’t need to copy the useTheme when you are not using it. This is just a customization for the Adornment we add in MuiInputText.

[Kevin Ilsen]

Thanks for making the enhancement. In addition to being able to customize the inputComponent, it’d be necessary to be able to pass in some properties that would be passed to the inputComponent (in order to pass the mask into the MaskedInput component, for example). I’m not sure the PR makes that possible.

If I see it correctly they can be handed over via the muiInputProps which are merged into the inputProps.

Before I begin, I’d like to say that I’m new to React, Material-UI and JsonForms so please excuse me if I say things that don’t make sense and/or are just plain incorrect :slight_smile: I’d stumbled across this post and immediately thought that this is exactly what I’m looking for. I need a way to enforce user input to conform to a specific format. As you’d mentioned, inputs for phone numbers, postal code, SSN and most importantly dollar amounts. In regards to the latter, I’d like the UI to show the input as $ 1,234.56 i.e., a dollar sign prefix, thousands separator and two decimal places.

Does your custom control allow for this? More importantly, I’m unsure about how I go about using this custom control. I mean, I’ve figured out how to add a <JsonForms …> component along with the props for the two schemas and data, but, I’m not sure how to make use of your custom component. I would greatly appreciate it if you could kindly show some sample code that does this.

Thank you for your contribution to making JsonForms even better.

Hi! To find out what I can do, say @discobot display help.

Is there any chance that I can get an example of this masked input in action?

I see the code attached above (thank you) but have no idea how to make it do its thing. I’ve got it in a separate file within my project. I understand that I’d need to add a mask option to the uischema but then what? How does the code get included to be executed?

Update: I dug further and found the example of using custom renders and tried to follow along. I needed to install and import the react-input-mask as mentioned in the original post. After doing that, I’m still not able to make this work :frowning:

I happen to notice that there’s this imaskjs/packages/react-imask at master · uNmAnNeR/imaskjs · GitHub and was wondering if it would help simplify setting up and using a masked input. Unfortunately I’m not skilled enough to help and was hoping someone more knowledgeable than I am could.

Hi @msueping,

can’t you just copy and paste the code above? It’s a bit older but I think it should still work. Other than that I can just recommend to proceed in small steps, especially when you don’t have yet so much experience with React, Material-UI and JsonForms.

My suggestion would be to implement a simple custom text control renderer to verify that your integration with JSON Forms is properly working. Then implement a simple masked input outside of JSON Forms and verify that it works like you intended. Then combine them to your final custom renderer.

Feel free to ask questions when you’re stuck. Ideally with the specific code which is not working and/or a reproducible example of the problems you’re facing.