Help with Color Picker implementation

Hi. I’m trying to implement a Color Picker for my React app, but I’m having trouble. I’m using a schema where I pass the “color” format to be used later by my custom renderer. But I can never render specifically the fields that should be Color Picker. In fact, it’s rendering the entire form incorrectly as if it were a Color Picker, which breaks the app.

Here is my Control:
import React from ‘react’;
import { TextField } from ‘@mui/material’;
import { SketchPicker } from ‘react-color’;

const ColorPickerControl = ({ data, handleChange }) => {
const handleColorChange = (color) => {
handleChange(color.hex);
};

return (
<div style={{ display: ‘flex’, alignItems: ‘center’ }}>
<TextField
type=“text”
value={data}
onChange={(e) => handleChange(e.target.value)}
style={{ marginRight: ‘10px’, width: ‘100px’ }}
/>


);
};

export default ColorPickerControl;

And my custom renderer:
const customRenderers = [
…materialRenderers,
{
tester: (uischema, schema) => {
if (schema && schema.format === ‘color’ && uischema.type === ‘Control’) {
return 3;
}
return -1;
},
renderer: ColorPickerControl
}
];

Hi @afcirillo96,

Can you also post your schema and uischema? Just from the code it’s not apparent why this happens.

Some things which caught my eye:

Your are not wrapping your custom renderer in the bindings of JSON Forms. Therefore you will only get some very basic props handed over. You should use withJsonFormsControlProps(ColorPickerControl). See here for example usage.

The tester are given the schema of the current level handed over which is the root schema in most cases. If you want to check properties on the subschema which correspond to the uischema control at hand, then you need to resolve it. We have the util schemaMatches for that. See here for example usage.

Here is my schema and uischema:
schema

{
  "type": "object",
  "properties": {
    "app": {
      "type": "object",
      "properties": {
        "version": { "type": "string" }
      }
    },
    "ui": {
      "type": "object",
      "properties": {
        "theme": {
          "type": "object",
          "properties": {
            "bodyBackground": { "type": "string", "format": "color" },
            "headerBackground": { "type": "string", "format": "color" },
            "menuBackground": { "type": "string", "format": "color" },
            "activeLayer": { "type": "string", "format": "color" },
            "textMenu": { "type": "string" },
            "textMenuStyle": { "type": "string" },
            "textLegendMenu": { "type": "string", "format": "color" },
            "textLegendMenuStyle": { "type": "string" },
            "iconBar": { "type": "string", "format": "color" }
          }
        }
      }
    },
    "resources": {
      "type": "object",
      "properties": {
        "leaflet": { "type": "string" }
      }
    }
  }
}

uischema

{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Group",
      "label": "App Configuration",
      "elements": [
        {
          "type": "Control",
          "scope": "#/properties/app/properties/version",
          "label": "App Version"
        }
      ]
    },
    {
      "type": "Group",
      "label": "UI Theme",
      "elements": [
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/bodyBackground",
          "label": "Body Background Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/headerBackground",
          "label": "Header Background Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/menuBackground",
          "label": "Menu Background Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/activeLayer",
          "label": "Active Layer Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/textMenu",
          "label": "Text Menu Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/textLegendMenu",
          "label": "Text Legend Menu Color"
        },
        {
          "type": "Control",
          "scope": "#/properties/ui/properties/theme/properties/iconBar",
          "label": "Icon Bar Color"
        }
      ]
    },
    {
      "type": "Group",
      "label": "Resources",
      "elements": [
        {
          "type": "Control",
          "scope": "#/properties/resources/properties/leaflet",
          "label": "Leaflet URL"
        }
      ]
    }
  ]
}

Thanks!

With that schema and uischema the ColorPickerControl should certainly not take over the rendering of all elements. Your tester for example checks for schema.format === 'color' which is certainly not true neither for the root schema nor for the app/version property. So there the usual VerticalLayout, Group and Control (string) renderers should take over.

I wrote this code and it seems to be working. Thanks for the help!

import React, { useState } from 'react';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { SketchPicker } from 'react-color'; 

const MaterialInputControl = ({ handleChange, data, path }) => {
  const [displayColorPicker, setDisplayColorPicker] = useState(false);
  const [currentColor, setCurrentColor] = useState(data || '#ffffff');

  const handleButtonClick = () => {
    setDisplayColorPicker(!displayColorPicker);
  };

  const handleColorChange = (color) => {
    setCurrentColor(color.hex);
    handleChange(path, color.hex);
  };

  return (
    <div>
      <button
        style={{
          backgroundColor: currentColor,
          width: '36px',
          height: '36px',
          border: 'none',
          cursor: 'pointer',
          borderRadius: '4px',
        }}
        onClick={handleButtonClick}
      />
      
      {displayColorPicker && (
        <div style={{ position: 'absolute', zIndex: 2 }}>
          <div
            style={{ position: 'fixed', top: 0, right: 0, bottom: 0, left: 0 }}
            onClick={() => setDisplayColorPicker(false)}
          />
          <SketchPicker
            color={currentColor}
            onChangeComplete={handleColorChange}
          />
        </div>
      )}
    </div>
  );
};

const ColorPickerControl = (props) => {
  const { handleChange, data, path } = props;
  return <MaterialInputControl handleChange={handleChange} data={data} path={path} />;
};

export default withJsonFormsControlProps(ColorPickerControl);

Looks good in general!

Generally speaking, local state should be avoided as much as possible. By managing the color value within the renderer, the renderer will not react on color changes coming from “outside” the form.

If you don’t have that use case then you’re fine and can leave it. If you have that use case, then you either need to react to data changes in your renderer, force a complete rerender to reset the local state or just get rid of the local state management.

Thanks for the advice! Now im getting this warnings in a lot of places:

unknown format "color" ignored in schema at path "#/properties/theme/properties/iconBar" 
unknown format "color" ignored in schema at path "#/properties/theme/properties/textLegendMenu"

etc, etc

Its not braking anything, but is really annoying.
Im managing the format color in my schema and im using it here:

  const colorPickerTester = rankWith(
    3,
    and(
      uiTypeIs('Control'),
      schemaMatches((schema) => schema.format === 'color')
    )
  );

Any idea on how to remove this warnings? Thanks!

Hi @afcirillo96,

That’s a warning/error by AJV as it doesn’t know the format. You can create a custom AJV instance in which you whitelist your formats and/or add an actual format: "color" validator.

Alternative you could store your format in a different property, e.g. x-my-format and use that one.

1 Like