Initialize custom component with data

Hello guys,

  1. My goal:
    I'm trying to create a reactive form as a custom component. It's something very similar to the existing JSON schema form, but I need it to support customized Quill Editor. My idea of it is to have the form as a parent component preserving the state of the controls it contains.

  2. Issue:
    This seem to work just fine for adding any input to any of the controls, but the issues start to pile up if I want to initialize the form with some pre-filled data. My idea was to have an initialValue property which would set the form value initially. The problem is that the hook responsible for that is triggered with basically any key stroke, which causes the data to be resetted constantly.

Is there any way to make the code run only when the initialValue property has been changed? Or is there any other way to make form work both ways?

  1. Additional info: (Cloud or Self-hosted, Screenshots):

The schema property value is:

{
schema: [
{key: 'test', label: 'test', type: 'text'},
{key: 'test1', label: 'test1', type: 'richtext'},
{key: 'test2', label: 'test2', type: 'richtext'},
{key: 'test3', label: 'test3', type: 'richtext'}
]
}

And the test initial value:

{
test: 'test simple text',
test1: {ops: [{insert: "This is the real active last line of defense 134\n"}]},
test2: {ops: [{insert: "This is the real active last line of defense 2\n"}]},
test3: {ops: [{insert: "This is the real active last line of defense 3\n"}]}
}

The component code:

const DynamicQuillEditor: FC<QuillEditorProps> = ({ onValueChange, initialValue, editorKey }) => {
  const editorRef = useRef<HTMLDivElement>(null); // Reference for the Quill container
  const quillInstance = useRef<Quill | null>(null); // Store the Quill instance
  const syncingContent = useRef(false); // To prevent feedback loops during updates

  useEffect(() => {
    // Initialize the Quill editor
    if (editorRef.current && !quillInstance.current) {
      quillInstance.current = new Quill(editorRef.current, {
        theme: 'snow',
        modules: {
          toolbar: [
            ['bold', 'italic', 'code'],
            [{ header: 1 }, { header: 2 }, 'code-block'],
            [{ list: 'ordered' }, { list: 'bullet' }],
            ['link'],
          ],
        },
        formats: [
          'bold',
          'code',
          'italic',
          'link',
          'header',
          'list',
          'code-block',
          'indent',
        ],
      });

      // Listen for user edits
      quillInstance.current.on('text-change', (ops, oldDelta, source) => {
        if (source === 'user' && quillInstance.current) {
          const delta = quillInstance.current.getContents();
          syncingContent.current = true; // Temporarily lock to avoid feedback loops
          onValueChange?.({ delta, key: editorKey });
        }
      });
    }
  }, []);

  const initEditorKey = useRef(editorKey);

  useEffect(() => {
    if (quillInstance.current && initialValue && !syncingContent.current) {
      const currentDelta = quillInstance.current.getContents();
      if (JSON.stringify(currentDelta) !== JSON.stringify(initialValue) && initEditorKey.current === editorKey) {
        // quillInstance.current.setContents(initialValue);
      }
    }
    syncingContent.current = false;
  }, [initialValue]);

  Retool.useComponentSettings({
    defaultHeight: 30,
    defaultWidth: 6,
  });

  return (
    <div style={{ height: '100%', width: '100%' }}>
      <div ref={editorRef} style={{ height: 'calc(100% - 42px)' }}/>
    </div>
  );
};

export const CustomDynamicForm: FC = () => {
  const [schema] = Retool.useStateObject({ name: 'schema' });
  const [values, setValues] = Retool.useStateObject({ name: 'values', inspector: 'text' });
  const [initialValues, setInitialValues] = Retool.useStateObject({ name: 'initialValues' });

  // Local state to do something on schema change
  const [formFields, setFormFields] = useState([]);

  useEffect(() => {
    if (Array.isArray(schema.schema)) {
      setFormFields(schema.schema);
    }
  }, [schema]);

  useEffect(() => {
    if (!!initialValues) {
      console.log('set initialValues', initialValues);
      // setValues(initialValues);
    }
  }, [initialValues]);

  const valuesRef = useRef(values);

  const handleChange = (key, value) => {
    const newValue = {
      ...(valuesRef.current ?? {}),
      [key]: value
    };
    setValues(newValue);
  };

  return (
    <div>
      <div>
        {formFields.map((field) => {
          switch (field.type) {
            case 'title':
              return (<h3>{field.label}</h3>);
            case 'richtext':
              return (
                <div key={field.key} className={'mb-2'}>
                  <label>{field.label}</label>
                  <DynamicQuillEditor
                    // initialValue={initialValues?.[field.key] ?? {}}
                                      fieldType={field.type}
                                      editorKey={field.key}
                                      onValueChange={(delta) => handleChange(field.key, delta?.delta)}
                  />
                </div>
              );
            case 'text':
            case 'number':
              return (
                <div key={field.key} className={'mb-2'}>
                  <label>{field.label}</label>
                  <input
                    type={field.type}
                    // value={initialValues?.[field.key] || ''}
                    onChange={(e) => handleChange(field.key, e.target.value)}
                  />
                </div>
              );
            default:
              return (
                <div key={field.key} className={'mb-2'}>
                  <label>{field.label}</label>
                  <input
                    type={field.type}
                    // value={values?.[field.key] || ''}
                    onChange={(e) => handleChange(field.key, e.target.value)}
                  />
                </div>
              );
          }
        })}
      </div>
    </div>
  );
};