Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to export the page as html #42

Open
DanyRupes opened this issue Feb 28, 2020 · 35 comments
Open

How to export the page as html #42

DanyRupes opened this issue Feb 28, 2020 · 35 comments

Comments

@DanyRupes
Copy link

I don't know how to export my page

@prevwong
Copy link
Owner

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
Copy link

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
Copy link
Contributor

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
Copy link

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

@Enva2712
Copy link
Contributor

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
Copy link

@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
Copy link

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
Copy link

        <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
Copy link

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
Copy link
Contributor

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
Copy link
Contributor

dbousamra commented Nov 24, 2020

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
Copy link

derit commented Nov 29, 2020

@dbousamra can you create an example in codesanbox?

@derit
Copy link

derit commented Nov 29, 2020

i think you forget getDescendants function from where?

@dbousamra
Copy link
Contributor

dbousamra commented Nov 30, 2020

@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
Copy link

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
Copy link

shakdoesgithub commented Feb 15, 2021

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
Copy link
Contributor

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
Copy link

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

@PurviJha
Copy link

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
Copy link

derit commented Jul 1, 2021

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
Copy link

PurviJha commented Jul 1, 2021

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
Copy link

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
Copy link

@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 =






${body}


; return html; };

@wbmag
Copy link

wbmag commented Apr 13, 2022

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
Copy link

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
Copy link

@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
Copy link

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

@tsaunde123
Copy link

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
Copy link

jorgegonzalez commented Apr 26, 2023

@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
Copy link

diegoddox commented May 5, 2023

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
Copy link

reboottime commented Jul 10, 2023

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

@jspasiuk
Copy link

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
Copy link

bzaman commented Apr 16, 2024

"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
Copy link

bzaman commented Apr 16, 2024

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

@qhkm
Copy link

qhkm commented Jul 5, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests