prevwong / craft.js

🚀 A React Framework for building extensible drag and drop page editors
https://craft.js.org
MIT License
7.4k stars 714 forks source link

How to export the page as html #42

Open DanyRupes opened 4 years ago

DanyRupes commented 4 years ago

I don't know how to export my page

prevwong commented 4 years ago

Unfortunately, this is currently out of our scope. For now, you can only serialize the editor state into JSON, and pass the JSON to the editor to reload the state. If you still need to export to HTML, you'll likely need to fiddle around with the JSON output and create your own HTML code generator.

jasonadkison commented 4 years ago

You could also build a separate version of your Editor and user components that render the editor state as "read only" and then select the element.outerHTML of the rendered DOM node you want.

Enva2712 commented 4 years ago

The Quill editor runs into similar problems when trying to render static markup. Luckily, React saves the day here with the ReactDOMServer.renderToStaticMarkup() method.

chungchi300 commented 4 years ago

@Enva2712 @jasonadkison Can you guys provide some sample code, please? It will be very helpful for this key common requirement.

Enva2712 commented 4 years ago

Sure, here's an example function that takes the editor's exported state and returns markup. It depends on the user components that the editor was given to produce the state.

import ReactDOMServer from 'react-dom/server';
import { Editor, Frame } from '@craftjs/core';
import userComponents from './user-components';

function renderMarkup(JSONStateString) {
  return ReactDOMServer.renderToStaticMarkup(<Editor enabled={false} resolver={userComponents}>
    <Frame json={JSONStateString} />
  </Editor>);
}
chungchi300 commented 4 years ago

@Enva2712 I have tried this but not lucks using the official landing page example, it returns an empty string

{
  "base64Str": "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",
  "json": `"{"canvas-ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["40","40","40","40"],"margin":["0","0","0","0"],"background":{"r":255,"g":255,"b":255,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"800px","height":"auto"},"parent":null,"nodes":["canvas-bevrghtgq"],"custom":{"displayName":"App"},"displayName":"Container"},"canvas-bevrghtgq":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"row","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["40","40","40","40"],"margin":["0","0","40","0"],"background":{"r":255,"g":255,"b":255,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"100%","height":"auto"},"parent":"canvas-ROOT","nodes":["canvas-dWPqjcHaB","canvas-QtJ8b6-Eg"],"custom":{"displayName":"Introduction"},"displayName":"Container"},"canvas-dWPqjcHaB":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["0","20","0","20"],"margin":["0","0","0","0"],"background":{"r":255,"g":255,"b":255,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"40%","height":"100%"},"parent":"canvas-bevrghtgq","nodes":["node-XTYy5V-6ur"],"custom":{"displayName":"Heading"},"displayName":"Container"},"canvas-QtJ8b6-Eg":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["0","20","0","20"],"margin":["0","0","0","0"],"background":{"r":255,"g":255,"b":255,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"60%","height":"100%"},"parent":"canvas-bevrghtgq","nodes":[],"custom":{"displayName":"Description"},"displayName":"Container"},"canvas-l949P_Ck0c":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"row","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["0","0","0","0"],"margin":["30","0","0","0"],"background":{"r":76,"g":78,"b":78,"a":0},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"100%","height":"auto"},"parent":"canvas-0KWCrWNmn","nodes":["canvas-hzB7Zr0Vvk","canvas-3Q32LXSUcg"],"custom":{"displayName":"Content"},"displayName":"Container"},"node-XTYy5V-6ur":{"type":{"resolvedName":"Text"},"props":{"fontSize":"23","textAlign":"left","fontWeight":"400","color":{"r":92,"g":90,"b":90,"a":1},"margin":[0,0,0,0],"shadow":0,"text":"Jeff is awesomeddsadjhiojoijoijioja"},"parent":"canvas-dWPqjcHaB","custom":{"displayName":"Text"},"displayName":"Text"},"canvas-XSWv35297I":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"yes","padding":["0","0","0","20"],"margin":["0","0","0","0"],"background":{"r":0,"g":0,"b":0,"a":0},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"55%","height":"100%"},"parent":"canvas-9n2A5pEPX","nodes":["node-tvZsTGgH-w","node-0MBnHeqPpa"],"custom":{"displayName":"Content"},"displayName":"Container"},"node-tuogLV8hNj":{"type":{"resolvedName":"Text"},"props":{"fontSize":"14","textAlign":"left","fontWeight":"400","color":{"r":92,"g":90,"b":90,"a":1},"margin":[0,0,0,0],"shadow":0,"text":"Govern what goes in and out of your components"},"parent":"canvas-B0C7EBoLM","custom":{"displayName":"Text"},"displayName":"Text"},"canvas-hzB7Zr0Vvk":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"row","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["0","20","0","0"],"margin":["0","0","0","0"],"background":{"r":0,"g":0,"b":0,"a":0},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"45%","height":"auto"},"parent":"canvas-l949P_Ck0c","nodes":["node-ccyOvn51tL"],"custom":{"displayName":"Left"},"displayName":"Container"},"canvas-3Q32LXSUcg":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["0","0","0","20"],"margin":["0","0","0","0"],"background":{"r":0,"g":0,"b":0,"a":0},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":0,"radius":0,"width":"55%","height":"auto"},"parent":"canvas-l949P_Ck0c","nodes":["node-RlIM3yp8Uu","node--RA_SY_JEt"],"custom":{"displayName":"Right"},"displayName":"Container"},"node-tvZsTGgH-w":{"type":{"resolvedName":"Text"},"props":{"fontSize":"20","textAlign":"left","fontWeight":"500","color":{"r":"255","g":"255","b":"255","a":"1"},"margin":["0","0","18","0"],"shadow":0,"text":"Design complex components"},"parent":"canvas-XSWv35297I","custom":{"displayName":"Text"},"displayName":"Text"},"node-0MBnHeqPpa":{"type":{"resolvedName":"Text"},"props":{"fontSize":"14","textAlign":"left","fontWeight":"400","color":{"r":"255","g":"255","b":"255","a":"0.8"},"margin":[0,0,0,0],"shadow":0,"text":"You can define areas within your React component which users can drop other components into. <br/><br />You can even design how the component should be edited — content editable, drag to resize, have inputs on toolbars — anything really."},"parent":"canvas-XSWv35297I","custom":{"displayName":"Text"},"displayName":"Text"},"node-ccyOvn51tL":{"type":{"resolvedName":"Custom1"},"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["20","20","20","20"],"margin":["0","0","0","0"],"background":{"r":119,"g":219,"b":165,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":40,"radius":0,"width":"100%","height":"auto"},"parent":"canvas-hzB7Zr0Vvk","_childCanvas":{"wow":"canvas-rsWrTbsnRt"},"custom":{"displayName":"Custom 1"},"displayName":"Custom 1"},"node-RlIM3yp8Uu":{"type":{"resolvedName":"Custom2"},"props":{"flexDirection":"row","alignItems":"center","justifyContent":"flex-start","fillSpace":"no","padding":["0","0","0","20"],"margin":["0","0","0","0"],"background":{"r":108,"g":126,"b":131,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":40,"radius":0,"width":"100%","height":"125px"},"parent":"canvas-3Q32LXSUcg","_childCanvas":{"wow":"canvas-Ad114XWFph"},"custom":{},"displayName":"Custom 2"},"node--RA_SY_JEt":{"type":{"resolvedName":"Custom3"},"props":{"flexDirection":"column","alignItems":"flex-start","justifyContent":"flex-start","fillSpace":"no","padding":["20","20","20","20"],"margin":["20","0","0","0"],"background":{"r":134,"g":187,"b":201,"a":1},"color":{"r":0,"g":0,"b":0,"a":1},"shadow":40,"radius":0,"width":"100%","height":"auto"},"parent":"canvas-3Q32LXSUcg","_childCanvas":{"wow":"canvas-NMH3A38bMs"},"custom":{},"displayName":"Custom 3"},"canvas-rsWrTbsnRt":{"type":{"resolvedName":"OnlyButtons"},"isCanvas":true,"props":{"children":[{"type":{"resolvedName":"Button"},"props":{}},{"type":{"resolvedName":"Button"},"props":{"buttonStyle":"outline","color":{"r":255,"g":255,"b":255,"a":1}}}],"className":"w-full mt-5"},"parent":"node-ccyOvn51tL","nodes":["node-EUPfH162gd","node-71k8cGarve"],"custom":{},"displayName":"OnlyButtons"},"canvas-Ad114XWFph":{"type":{"resolvedName":"Custom2VideoDrop"},"isCanvas":true,"props":{"children":[{"type":{"resolvedName":"Video"},"props":{}}],"className":"flex-1 ml-5 h-full"},"parent":"node-RlIM3yp8Uu","nodes":["node-IV4oYPDURM"],"custom":{},"displayName":"Custom2VideoDrop"},"canvas-NMH3A38bMs":{"type":{"resolvedName":"Custom3BtnDrop"},"isCanvas":true,"props":{"children":[{"type":{"resolvedName":"Button"},"props":{"background":{"r":184,"g":247,"b":247,"a":1}}}],"className":"w-full h-full"},"parent":"node--RA_SY_JEt","nodes":["node-sQTl2cXS4P"],"custom":{},"displayName":"Custom3BtnDrop"},"node-EUPfH162gd":{"type":{"resolvedName":"Button"},"props":{"background":{"r":255,"g":255,"b":255,"a":0.5},"color":{"r":92,"g":90,"b":90,"a":1},"buttonStyle":"full","text":"Button","margin":["5","0","5","0"],"textComponent":{"fontSize":"15","textAlign":"center","fontWeight":"500","color":{"r":92,"g":90,"b":90,"a":1},"margin":[0,0,0,0],"shadow":0,"text":"Text"}},"parent":"canvas-rsWrTbsnRt","custom":{},"displayName":"Button"},"node-71k8cGarve":{"type":{"resolvedName":"Button"},"props":{"background":{"r":255,"g":255,"b":255,"a":0.5},"color":{"r":255,"g":255,"b":255,"a":1},"buttonStyle":"outline","text":"Button","margin":["5","0","5","0"],"textComponent":{"fontSize":"15","textAlign":"center","fontWeight":"500","color":{"r":92,"g":90,"b":90,"a":1},"margin":[0,0,0,0],"shadow":0,"text":"Text"}},"parent":"canvas-rsWrTbsnRt","custom":{},"displayName":"Button"},"node-IV4oYPDURM":{"type":{"resolvedName":"Video"},"props":{"videoId":"IwzUs1IMdyQ"},"parent":"canvas-Ad114XWFph","custom":{},"displayName":"Video"},"node-sQTl2cXS4P":{"type":{"resolvedName":"Button"},"props":{"background":{"r":184,"g":247,"b":247,"a":1},"color":{"r":92,"g":90,"b":90,"a":1},"buttonStyle":"full","text":"Button","margin":["5","0","5","0"],"textComponent":{"fontSize":"15","textAlign":"center","fontWeight":"500","color":{"r":92,"g":90,"b":90,"a":1},"margin":[0,0,0,0],"shadow":0,"text":"Text"}},"parent":"canvas-NMH3A38bMs","custom":{},"displayName":"Button"}}"`
}
abdhalees commented 4 years ago

this works fine with me

function CraftEditor = () => {
  const ref = useRef(null)
    return (
      <Editor>
        <div ref={ref}>
           <Frame>
             <Canvas>
             </Canvas>
            </Frame>
         </div>
      </Editor>
   )
}
  const html = ref.current.firstChild.firstChild.outerHTML
khusseini commented 4 years ago
        <div ref={ref}>

Do you maybe have a more elaborate example? Like how would I be able to access the ref from a different component? (like an export button)

abdhalees commented 4 years ago

Do you maybe have a more elaborate example? Like how would I be able to access the ref from a different component? (like an export button)

@khusseini if it's a child pass it as a prop

dbousamra commented 3 years ago

Does anyone have any working methods for generating HTML strings on the server? I tried renderToStaticMarkup but it gives am empty string. Do I need to write my own function?

dbousamra commented 3 years ago

Alright, I have a working method to take some serialized nodes, and generate HTML. Our use case is that we need to render PDF's using an external service that takes HTML as an input. The main issue is the use of useEffect within CraftJS. Using useEffect with React's renderToStaticMarkup is a no-op. Here is some code that I use to render static HTML

Replacement Element component, that will only use CraftJS's Element type if not in SSR mode (i.e. where we have useEffect available). I use this everywhere i construct Elements manually (toolbox etc).

import { NodeId, Element as CraftJsElement } from '@craftjs/core';
import React from 'react';

export type Element<T extends React.ElementType> = {
  id?: NodeId;
  is?: T;
  custom?: Record<string, any>;
  children?: React.ReactNode;
  canvas?: boolean;
  isSSR?: boolean;
} & React.ComponentProps<T>;

export function Element<T extends React.ElementType>({
  is,
  id,
  children,
  isSSR,
  ...elementProps
}: Element<T>): JSX.Element {
  return isSSR ? (
    React.createElement(is, elementProps, children)
  ) : (
    <CraftJsElement id={id} {...elementProps}>
      {children}
    </CraftJsElement>
  );
}

Util functions

export type SerializedNodeWithId = SerializedNode & { id: string };

export const deserializeNodes = (nodes: SerializedNodes): SerializedNodeWithId[] => {
  return Object.entries(nodes).map(([id, val]) => ({ id, ...val }));
};

export const getNodeById = (nodes: SerializedNodeWithId[], id: NodeId) => {
  return _.find(nodes, (node) => node.id === id);
};

export function getDescendants(
  nodes: SerializedNodeWithId[],
  id: NodeId,
  deep = false,
  includeOnly?: 'linkedNodes' | 'childNodes',
): SerializedNodeWithId[] {
  function appendChildNode(id: NodeId, descendants: NodeId[] = [], depth: number = 0) {
    if (deep || (!deep && depth === 0)) {
      const node = getNodeById(nodes, id);

      if (!node) {
        return descendants;
      }

      if (includeOnly !== 'childNodes') {
        // Include linkedNodes if any
        const linkedNodes = node.linkedNodes;

        _.each(linkedNodes, (nodeId) => {
          descendants.push(nodeId);
          descendants = appendChildNode(nodeId, descendants, depth + 1);
        });
      }

      if (includeOnly !== 'linkedNodes') {
        const childNodes = node.nodes;

        _.each(childNodes, (nodeId) => {
          descendants.push(nodeId);
          descendants = appendChildNode(nodeId, descendants, depth + 1);
        });
      }

      return descendants;
    }
    return descendants;
  }
  return _.compact(_.map(appendChildNode(id), (nid) => getNodeById(nodes, nid)));
}

export const renderNode = (
  nodes: SerializedNodeWithId[],
  resolver: Resolver,
  nodeId: NodeId,
): JSX.Element => {
  const node = getNodeById(nodes, nodeId);

  if (!node) {
    throw new Error(`Could not find node with id ${nodeId}`);
  }

  const resolvedComponent = _.get(resolver, (node.type as any).resolvedName);
  const descendants = getDescendants(nodes, nodeId);
  const children = _.map(descendants, (descendant) => renderNode(nodes, resolver, descendant.id));

  return (
    <NodeProvider key={node.id} id={node.id}>
      {React.createElement(resolvedComponent, { ...node.props, isSSR: true }, children)}
    </NodeProvider>
  );
};

export const renderNodesToJSX = (
  nodes: SerializedNodeWithId[],
  resolver: Resolver,
  nodeId: NodeId,
): JSX.Element => {
  return (
    <Editor enabled={false} resolver={resolver}>
      {renderNode(nodes, resolver, nodeId)}
    </Editor>
  );
};

You can use it like so:

const nodes = deserializeNodes(serializedNodesFromDb);
const jsx = renderNodesToJSX(nodes, RESOLVERS, 'ROOT');
const html = ReactDOMServer.renderToString(jsx);

I hope this helps anyone looking to render out a non-editable static representation of a CraftJS node tree.

derit commented 3 years ago

@dbousamra can you create an example in codesanbox?

derit commented 3 years ago

i think you forget getDescendants function from where?

dbousamra commented 3 years ago

@dbousamra can you create an example in codesanbox?

I can, but not right now. I'll do after work.

i think you forget getDescendants function from where?

Thanks. I've added it

lord007tn commented 3 years ago

Alright, I have a working method to take some serialized nodes, and generate HTML. Our use case is that we need to render PDF's using an external service that takes HTML as an input. The main issue is the use of useEffect within CraftJS. Using useEffect with React's renderToStaticMarkup is a no-op. Here is some code that I use to render static HTML

Replacement Element component, that will only use CraftJS's Element type if not in SSR mode (i.e. where we have useEffect available). I use this everywhere i construct Elements manually (toolbox etc).

import { NodeId, Element as CraftJsElement } from '@craftjs/core';
import React from 'react';

export type Element<T extends React.ElementType> = {
  id?: NodeId;
  is?: T;
  custom?: Record<string, any>;
  children?: React.ReactNode;
  canvas?: boolean;
  isSSR?: boolean;
} & React.ComponentProps<T>;

export function Element<T extends React.ElementType>({
  is,
  id,
  children,
  isSSR,
  ...elementProps
}: Element<T>): JSX.Element {
  return isSSR ? (
    React.createElement(is, elementProps, children)
  ) : (
    <CraftJsElement id={id} {...elementProps}>
      {children}
    </CraftJsElement>
  );
}

Util functions

export type SerializedNodeWithId = SerializedNode & { id: string };

export const deserializeNodes = (nodes: SerializedNodes): SerializedNodeWithId[] => {
  return Object.entries(nodes).map(([id, val]) => ({ id, ...val }));
};

export const getNodeById = (nodes: SerializedNodeWithId[], id: NodeId) => {
  return _.find(nodes, (node) => node.id === id);
};

export function getDescendants(
  nodes: SerializedNodeWithId[],
  id: NodeId,
  deep = false,
  includeOnly?: 'linkedNodes' | 'childNodes',
): SerializedNodeWithId[] {
  function appendChildNode(id: NodeId, descendants: NodeId[] = [], depth: number = 0) {
    if (deep || (!deep && depth === 0)) {
      const node = getNodeById(nodes, id);

      if (!node) {
        return descendants;
      }

      if (includeOnly !== 'childNodes') {
        // Include linkedNodes if any
        const linkedNodes = node.linkedNodes;

        _.each(linkedNodes, (nodeId) => {
          descendants.push(nodeId);
          descendants = appendChildNode(nodeId, descendants, depth + 1);
        });
      }

      if (includeOnly !== 'linkedNodes') {
        const childNodes = node.nodes;

        _.each(childNodes, (nodeId) => {
          descendants.push(nodeId);
          descendants = appendChildNode(nodeId, descendants, depth + 1);
        });
      }

      return descendants;
    }
    return descendants;
  }
  return _.compact(_.map(appendChildNode(id), (nid) => getNodeById(nodes, nid)));
}

export const renderNode = (
  nodes: SerializedNodeWithId[],
  resolver: Resolver,
  nodeId: NodeId,
): JSX.Element => {
  const node = getNodeById(nodes, nodeId);

  if (!node) {
    throw new Error(`Could not find node with id ${nodeId}`);
  }

  const resolvedComponent = _.get(resolver, (node.type as any).resolvedName);
  const descendants = getDescendants(nodes, nodeId);
  const children = _.map(descendants, (descendant) => renderNode(nodes, resolver, descendant.id));

  return (
    <NodeProvider key={node.id} id={node.id}>
      {React.createElement(resolvedComponent, { ...node.props, isSSR: true }, children)}
    </NodeProvider>
  );
};

export const renderNodesToJSX = (
  nodes: SerializedNodeWithId[],
  resolver: Resolver,
  nodeId: NodeId,
): JSX.Element => {
  return (
    <Editor enabled={false} resolver={resolver}>
      {renderNode(nodes, resolver, nodeId)}
    </Editor>
  );
};

You can use it like so:

const nodes = deserializeNodes(serializedNodesFromDb);
const jsx = renderNodesToJSX(nodes, RESOLVERS, 'ROOT');
const html = ReactDOMServer.renderToString(jsx);

I hope this helps anyone looking to render out a non-editable static representation of a CraftJS node tree.

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

shakdoesgithub commented 3 years ago

I have tackled this issue by just creating another page which reads the serialized nodes and renders the tree. Here's the example:


import { TextReadOnly as Text } from "./components/text";
import { ButtonReadOnly as Button } from "./components/button";
import { ContainerReadOnly as Container } from "./components/container";
import { ImageReadOnly as Image } from "./components/image";

// this is your serialized json
// I've saved it locally but you can read it from anywhere
import json from "./example.json";

// This gets called on every request
export async function getServerSideProps() {
  return { props: { data: json } };
}

const Preview = ({ data }: any) => {
  return (
    <div className="h-screen flex flex-col ">
      <Node node={data.ROOT} data={data} />
    </div>
  );
};

const Node = ({ node, data }) => {
  let typeName = "";
  if (typeof node.type === "object") {
    typeName = node.type.resolvedName;
  } else {
    typeName = node.type;
  }

  const Children = node.nodes.map((x, index) => {
    return <Node key={x} node={data[x]} data={data} />;
  });
  switch (typeName) {
    case "Container":
      return <Container {...node.props}>{Children}</Container>;
    case "Text":
      return <Text {...node.props} />;
    case "Button":
      return <Button {...node.props} />;
    case "Image":
      return <Image {...node.props} />;
  }
};
export default Preview;
dbousamra commented 3 years ago

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

https://codesandbox.io/s/keen-fast-m3y2z?file=/src/index.tsx

hugominas commented 3 years ago

I believe this is so trivial that it should be core feature @dbousamra any change we can open a new PR?

PurviJha commented 3 years ago

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

https://codesandbox.io/s/keen-fast-m3y2z?file=/src/index.tsx

this is not working with js project facing problem with lodash

derit commented 3 years ago

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

https://codesandbox.io/s/keen-fast-m3y2z?file=/src/index.tsx

this is not working with js project facing problem with lodash

it's, working, just check the result image

PurviJha commented 3 years ago

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

https://codesandbox.io/s/keen-fast-m3y2z?file=/src/index.tsx

this is not working with js project facing problem with lodash

it's, working, just check the result image

I have my project in jsx and when I tried to add this file I face lodash error

rishii1909 commented 2 years ago

Is there a solution for this? I've been stuck for 3 days with no progress, and I'm in the last stage of my project timeline, can't switch everything now : (

rishii1909 commented 2 years ago

@derit Any way there can be a jsx implementation, I have tried for a js workaround, but it fails with error "Cannot read id of null" at renderTostaticMarkup call

"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateHtml = exports.renderNode = exports.getDescendants = exports.getNodeById = void 0; var lodash_1 = require("lodash"); var react_1 = require("react"); var server_1 = require("react-dom/server"); var Resolver_1 = require("./Resolver"); var RESOLVERS = Resolver_1.Resolvers; exports.getNodeById = function (nodes, id) { console.log('returning getNodeByID'); return lodash_1.find(nodes, function (node) { return node.id === id; }); }; var deserializeNodes = function (nodes, id, sorted) { if (id === void 0) { id = "ROOT"; } if (sorted === void 0) { sorted = []; } var node = nodes[id] if (!node) { var node = JSON.parse(nodes)[id] console.log("Error : Could not find node " + id); } sorted.push(__assign({ id: id }, node)); lodash_1.each(node.nodes, function (n) { sorted.push.apply(sorted, deserializeNodes(nodes, n)); }); console.log("SORTED", sorted) return sorted; }; function getDescendants(nodes, id, deep, includeOnly) { if (deep === void 0) { deep = false; } function appendChildNode(id, descendants, depth) { if (descendants === void 0) { descendants = []; } if (depth === void 0) { depth = 0; } if (deep || (!deep && depth === 0)) { var node = exports.getNodeById(nodes, id); if (!node) { return descendants; } if (includeOnly !== "childNodes") { // Include linkedNodes if any var linkedNodes = node.linkedNodes; lodash_1.each(linkedNodes, function (nodeId) { descendants.push(nodeId); descendants = appendChildNode(nodeId, descendants, depth + 1); }); } if (includeOnly !== "linkedNodes") { var childNodes = node.nodes; lodash_1.each(childNodes, function (nodeId) { descendants.push(nodeId); descendants = appendChildNode(nodeId, descendants, depth + 1); }); } return descendants; } return descendants; } return lodash_1.compact(lodash_1.map(appendChildNode(id), function (nid) { return exports.getNodeById(nodes, nid); })); } exports.getDescendants = getDescendants; exports.renderNode = function (nodes, resolver, nodeId) { var node = exports.getNodeById(nodes, nodeId); if (!node) { throw new Error("Could not find node with id " + nodeId); } var resolvedComponent = lodash_1.get(resolver, node.type.resolvedName); var descendants = getDescendants(nodes, nodeId); var children = lodash_1.map(descendants, function (descendant) { console.log('returning children', descendant.id); return exports.renderNode(nodes, resolver, descendant.id); }); if (!resolvedComponent) { console.log("resolvedComponent failed for",node) resolvedComponent = node.type }else{ console.log("resolvedComponent success",node.props) } // console.log("RENDER NODE OUTPUT", node, resolvedComponent, children) return react_1.createElement(resolvedComponent, __assign(__assign({}, node.props), { isSSR: true, id: nodeId }), children); }; var renderNodesToJSX = function (nodes, resolver, nodeId) { return exports.renderNode(nodes, resolver, nodeId); }; exports.generateHtml = function (craftJsNodes) { var nodes = deserializeNodes(craftJsNodes); var jsx = renderNodesToJSX(nodes, RESOLVERS, "ROOT"); console.log("generateJSX",jsx); var body = server_1.renderToStaticMarkup(<div>{ jsx }</div>); console.log("GENERATED BODY : ", body) var html = <!DOCTYPE html>

${body}
`;
return html;

}; `

wbmag commented 2 years ago

I have tackled this issue by just creating another page which reads the serialized nodes and renders the tree. Here's the example:


import { TextReadOnly as Text } from "./components/text";
import { ButtonReadOnly as Button } from "./components/button";
import { ContainerReadOnly as Container } from "./components/container";
import { ImageReadOnly as Image } from "./components/image";

// this is your serialized json
// I've saved it locally but you can read it from anywhere
import json from "./example.json";

// This gets called on every request
export async function getServerSideProps() {
  return { props: { data: json } };
}

const Preview = ({ data }: any) => {
  return (
    <div className="h-screen flex flex-col ">
      <Node node={data.ROOT} data={data} />
    </div>
  );
};

const Node = ({ node, data }) => {
  let typeName = "";
  if (typeof node.type === "object") {
    typeName = node.type.resolvedName;
  } else {
    typeName = node.type;
  }

  const Children = node.nodes.map((x, index) => {
    return <Node key={x} node={data[x]} data={data} />;
  });
  switch (typeName) {
    case "Container":
      return <Container {...node.props}>{Children}</Container>;
    case "Text":
      return <Text {...node.props} />;
    case "Button":
      return <Button {...node.props} />;
    case "Image":
      return <Image {...node.props} />;
  }
};
export default Preview;

This is slightly confusing. Did you create new readonly versions (TextReadOnly, ButtonReadOnly, etc) for each component?

khanhleemtp commented 2 years ago

I'm facing this problem, and i foune your answer here if you can provide a sandbox that would be very helpful.

https://codesandbox.io/s/keen-fast-m3y2z?file=/src/index.tsx

this is not working with js project facing problem with lodash

it's, working, just check the result image

I have exported HTML from JSON. But i don't see js in file. How can do it ?

mattvb91 commented 1 year ago

@wbmag ive just implemented that solution and yes basically its the same component but without the useNode() hook in it (which is causing the ssr issue.

hugominas commented 1 year ago

@mattvb91 so you could pass a SSR flag to the same component, to either read the props from useNode or from props?

tsaunde123 commented 1 year ago

Hi all, I'm running into issues trying to replicate @dbousamra 's solution on this reply.

I'm trying to export the page as HTML on every node change using the Editor's onNodesChange callback. But I get an error that states i'm attempting to use the useEditor hook outside the Editor component. Not sure where i'm going wrong.

The error gets thrown when calling ReactDOMServer.renderToString(jsx); from @dbousamra 's utils functions.

See below where i'm calling generateHtml and the error I get.

Any help would be appreciated!

<Editor
        resolver={{ Prompt, Button, Text, Container }}
        // Save the updated prompt whenever the Nodes has been changed
        onNodesChange={(query: QueryMethods) => {
          const json: string = query.serialize();
          const nodes: SerializedNodes = query.getSerializedNodes();

          const render: string = generateHtml(nodes);

          // save to server
        }}
      >

However when calling generateHtml i get the following error:

react-dom.development.js?ac89:4312 Uncaught Error: Invariant failed: You can only use useEditor in the context of <Editor />. 

Please only use useEditor in components that are children of the <Editor /> component.
    at invariant (tiny-invariant.js?b434:12:1)
    at re (index.js?076b:15:1487)
    at fe (index.js?076b:15:3725)
    at renderWithHooks (react-dom-server-legacy.browser.development.js?2e2d:5661:1)
    at renderIndeterminateComponent (react-dom-server-legacy.browser.development.js?2e2d:5734:1)
    at renderElement (react-dom-server-legacy.browser.development.js?2e2d:5949:1)
    at renderNodeDestructiveImpl (react-dom-server-legacy.browser.development.js?2e2d:6107:1)
    at renderNodeDestructive (react-dom-server-legacy.browser.development.js?2e2d:6079:1)
    at renderIndeterminateComponent (react-dom-server-legacy.browser.development.js?2e2d:5788:1)
    at renderElement (react-dom-server-legacy.browser.development.js?2e2d:5949:1)
    at renderNodeDestructiveImpl (react-dom-server-legacy.browser.development.js?2e2d:6107:1)
    at renderNodeDestructive (react-dom-server-legacy.browser.development.js?2e2d:6079:1)
    at retryTask (react-dom-server-legacy.browser.development.js?2e2d:6531:1)
    at performWork (react-dom-server-legacy.browser.development.js?2e2d:6579:1)
    at eval (react-dom-server-legacy.browser.development.js?2e2d:6903:1)
    at scheduleWork (react-dom-server-legacy.browser.development.js?2e2d:77:1)
    at startWork (react-dom-server-legacy.browser.development.js?2e2d:6902:1)
    at renderToStringImpl (react-dom-server-legacy.browser.development.js?2e2d:6976:1)
    at Object.renderToString (react-dom-server-legacy.browser.development.js?2e2d:6997:1)
    at generateHtml (utils.ts?f745:133:16)
    at onExport (VM74676 Topbar.tsx:25:64)
    at HTMLUnknownElement.callCallback (react-dom.development.js?ac89:4164:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?ac89:4213:1)
    at invokeGuardedCallback (react-dom.development.js?ac89:4277:1)
    at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js?ac89:4291:1)
    at executeDispatch (react-dom.development.js?ac89:9041:1)
    at processDispatchQueueItemsInOrder (react-dom.development.js?ac89:9073:1)
    at processDispatchQueue (react-dom.development.js?ac89:9086:1)
    at dispatchEventsForPlugins (react-dom.development.js?ac89:9097:1)
    at eval (react-dom.development.js?ac89:9288:1)
    at batchedUpdates$1 (react-dom.development.js?ac89:26140:1)
    at batchedUpdates (react-dom.development.js?ac89:3991:1)
    at dispatchEventForPluginEventSystem (react-dom.development.js?ac89:9287:1)
    at dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay (react-dom.development.js?ac89:6465:1)
    at dispatchEvent (react-dom.development.js?ac89:6457:1)
    at dispatchDiscreteEvent (react-dom.development.js?ac89:6430:1)
jorgegonzalez commented 1 year ago

@tsaunde123 Did you ever succeed with this problem? In the same place you were with that solution You can only use useNode in the context of <Editor />.

edit: For posterity, I solved this by replacing all the useNode and useEditor calls within the subcomponents that are trying to be rendered as raw html, with the useSSRNode and useSSREditor above

diegoddox commented 1 year ago

I created this small solution that, should work without being in the editor context, the method takes the serialized object and transforms into an HTML, note if the element is not on the object it will not be created.

Keep in mind, the way in which craftjs serialize the node, there is one limitation afaik. When creating for ex a Card.tsx and adding via the action.add the first element in that component will be added as a linkedNodes and no props will be propagated to the serialize object

Maybe something that we could change in order to have a full conversion to HTML from the JSON object, the workaround for me now is to add the first element as a div with no props.

reboottime commented 1 year ago

I added a POC solution using Next.js https://github.com/reboottime/craftjs-nextjs-ssr-poc after testing some of above ideas.

jspasiuk commented 10 months ago

I added a POC solution using Next.js https://github.com/reboottime/craftjs-nextjs-ssr-poc after testing some of above ideas.

Links not working

bzaman commented 3 months ago
"use client";

import { Editor, Frame } from "@craftjs/core";

// all the user component
import * as UserComponents from "../../components/user";

// your saved json
const pTemplate = require("@/data/page1.json");

export default function Page() {
  return (
    <div className="relative" style={{ maxWidth: 700, marginInline: "auto" }}>
      <div>
        <Editor enabled={false} resolver={UserComponents}>
          <Frame data={pTemplate} />
        </Editor>
      </div>
    </div>
  );
}
bzaman commented 3 months ago

It working great, let me know if anybody need any help

qhkm commented 2 weeks ago

@bzaman that's not in plain html format. The discussion topic above was about how to transform the serialized json to plain html IMO