codex-team / editor.js

A block-style editor with clean JSON output
https://editorjs.io
Apache License 2.0
28.48k stars 2.08k forks source link

Editor render issue with data props #2509

Open altumsoftwareds opened 1 year ago

altumsoftwareds commented 1 year ago

I am waiting when I will get response with some structure and I need to pass it to Editor.

( I also see such error in console: Block «paragraph» skipped because saved data is invalid )

so on initial render editor triggers onChange method with empty data {time: 1697134792817, blocks: Array(0), version: '2.28.0'} ( I tested it using setTimeout )

and it overrides data I save in useState. That's why I see empty editor.

Full code :

import React from "react";
import { Button, styled, TextField } from "@mui/material";
import { PolicyCybertaskContext } from "..";
import { Column, Row } from "components/containers";
import Widget from "components/widget";
import { CopyAll } from "@mui/icons-material";
import {
    UpdatePolicySectionDtoInput,
    useUpdatePolicySectionMutation,
} from "lib/apollo/types";
import dynamic from "next/dynamic";
import { editorDataTransform } from "lib/utils/editorDataTransform";

interface Props {
    onPrev: () => void;
    onNext: () => void;
    section: number;
    item: number;
}

const Editor = dynamic(() => import("components/Editor"), {
    ssr: false,
  });

const PolicySections: React.FC<Props> = ({ onPrev, onNext, section, item }) => {
    const { template, policy } = React.useContext(PolicyCybertaskContext);
    const [error, setError] = React.useState<string>();
    const [answer, setAnswer] = React.useState<any>({});
    const [updateSection] = useUpdatePolicySectionMutation();

    React.useEffect(() => {
        if (!policy || !policy.policySections[section]) {
            setAnswer({});
            return;
        }
        const currentItem = policy.policySections[section].items[item];
        if (!currentItem) {
            setAnswer({});
        } else {
            setAnswer(editorDataTransform(currentItem));
        }
    }, [section, item, policy]);

    const save = () => {
        setError(undefined);
        if (!policy?.policySections[section]) return;

        const dto: UpdatePolicySectionDtoInput = {
            id: policy.policySections[section].id,
            items: policy.policySections[section].items[item]
                ? policy.policySections[section].items.map((a, index) => {
                      if (index === item) {
                          return JSON.stringify(answer);
                      }
                      return a;
                  })
                : [...policy.policySections[section].items, JSON.stringify(answer)],
        };

        updateSection({ variables: { dto } });
    };

    const saveAndGoBack = () => {
        if (answer.length === 0) {
            setError("This field is required.");
            return;
        }
        save();
        onPrev();
    };

    const saveAndContinue = () => {
        if (answer.blocks.length === 0) {
            setError("This field is required.");
            return;
        }
        save();
        onNext();
    };

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setError(undefined);
        setAnswer(e.target.value);
    };

    const copyExample = () => {
        if (
            template?.templatePolicy.templatePolicySections[section]
                .templatePolicySectionItems[item].exampleAnswer
        )
            setAnswer(
                template?.templatePolicy.templatePolicySections[section]
                    .templatePolicySectionItems[item].exampleAnswer
            );
    };

    console.log(policy?.policySections)
    console.log(item)
    console.log(answer)

    return (
        <Container>
            <QuestionBox color="primary">
                {
                    template?.templatePolicy.templatePolicySections[section]
                        .templatePolicySectionItems[item].question
                }
            </QuestionBox>
            <Row>
                <Widget color="complimentary3.lightest" sx={{ width: "50%" }}>
                    <HeaderRow>
                        <h4>Example answer</h4>
                        <Button
                            variant="text"
                            sx={{ padding: "0" }}
                            onClick={copyExample}
                        >
                            <CopyAll />
                        </Button>
                    </HeaderRow>
                    {template?.templatePolicy.templatePolicySections[
                        section
                    ].templatePolicySectionItems[item].exampleAnswer
                        .split("\n")
                        .map((line, i) => (
                            <p key={i}>{line}</p>
                        ))}
                </Widget>
                <Widget color="complimentary3.lightest" sx={{ width: "50%" }}>
                    <Row>
                        <h4>Your answer</h4>
                    </Row>
                    <p>{JSON.stringify(answer)}</p>
                    <Editor data={answer} onChange={(data) => {
                        console.log('data', data)
                        setAnswer(data)
                        // if(data.blocks.length !== 0){
                        //     setTimeout(()=> {setAnswer(data)},3000)
                        // }
                    }} editorblock={`editorjs-container-${section}`}/>
                </Widget>
            </Row>
        </Container>
    );
};

Безымянный

altumsoftwareds commented 1 year ago

If I will pass expected data directly as prop ( not waiting for response ) -- there is no issues at all.

altumsoftwareds commented 1 year ago

@neSpecc could you help, please? 1) Безымянный

2) Безымянный

1) onChange triggers with empty data however current answer is exists and it rewrites answer to be empty 2) even after some additional rerenders of component ( as soon as I get requested from BE data from props ) current answer becomes correct again, but editor doesn't rerenders with correct answer ( data ) object

altumsoftwareds commented 1 year ago
<Editor 
                        key={`editor-${section}-${item}`} 
                        data={answer} 
                        onChange={(data) => {
                            console.log('editor beforeOnChange answer', answer)
                            console.log('editor onChange', data)
                            setAnswer(data)
                        }}
                        editorblock={`editorjs-container-${section}-${item}`} 
                    />

why I don't see current answer after onChange as soon as I have setAnswer which is the part of useState?

neSpecc commented 12 months ago

@altumsoftwareds would you test in it 2.29.0-rc.4?

ls84 commented 11 months ago

I am not sure if this is related, I am getting the same "warning" when I put editor.save() method inside the onChange function. However It is fine when I trigger editor.save() somewhere else

editor = new EditorJS({
    holder : 'summary',
    onChange: function(api, event) {
        editor.save().then(console.log)
    }
});
altumsoftwareds commented 11 months ago

@ls84 where to put editor.save() else, for example?

ls84 commented 11 months ago

@ls84 where to put editor.save() else, for example?

I am guessing the problem of Block «paragraph» skipped because saved data is invalid is caused by saving data before it's ready to be saved out. My solution was to bind editor.save() to a button click action. Hope this helps

a3626a commented 10 months ago

It's because the Paragraph delays saving its data to element. And the save function makes data from its element. (And the warning occurs when the element has empty body)

requestAnimationFrame causes the problem.

https://github.com/editor-js/paragraph/blob/6fff362a536599ba1f05a9dfd642d51668ac557b/src/index.js#L239-L254

There are two solutions.

1) Override the Paragraph and remove 'requestAnimationFrame' logic. 2) Save using requestAnimationFrame inside onChange. This will make the save is called after hydrate. (FIFO)