craft.js icon indicating copy to clipboard operation
craft.js copied to clipboard

How to export the page as html

Open DanyRupes opened this issue 5 years ago • 38 comments

I don't know how to export my page

DanyRupes avatar Feb 28 '20 08:02 DanyRupes

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.

prevwong avatar Feb 28 '20 13:02 prevwong

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.

jasonadkison avatar Feb 28 '20 21:02 jasonadkison

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

rusmisel avatar Mar 12 '20 21:03 rusmisel

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

chungchi300 avatar Mar 15 '20 06:03 chungchi300

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>);
}

rusmisel avatar Mar 15 '20 16:03 rusmisel

@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"}}"`
}

chungchi300 avatar Mar 19 '20 11:03 chungchi300

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

abdhalees avatar Mar 31 '20 10:03 abdhalees

        <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)

khusseini avatar Jul 23 '20 19:07 khusseini

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

abdhalees avatar Jul 25 '20 16:07 abdhalees

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 avatar Nov 24 '20 07:11 dbousamra

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.

dbousamra avatar Nov 24 '20 21:11 dbousamra

@dbousamra can you create an example in codesanbox?

derit avatar Nov 29 '20 10:11 derit

i think you forget getDescendants function from where?

derit avatar Nov 29 '20 11:11 derit

@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

dbousamra avatar Nov 30 '20 01:11 dbousamra

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.

lord007tn avatar Feb 15 '21 13:02 lord007tn

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;

shakdoesgithub avatar Feb 15 '21 16:02 shakdoesgithub

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

dbousamra avatar Mar 11 '21 03:03 dbousamra

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

hugominas avatar Jun 25 '21 09:06 hugominas

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

PurviJha avatar Jun 30 '21 11:06 PurviJha

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

derit avatar Jul 01 '21 17:07 derit

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

PurviJha avatar Jul 01 '21 18:07 PurviJha

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 avatar Aug 16 '21 05:08 rishii1909

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

<meta charSet="UTF-8" /> ${body} ; return html; };

rishii1909 avatar Aug 16 '21 05:08 rishii1909

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?

wbmag avatar Apr 13 '22 17:04 wbmag

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 ?

khanhleemtp avatar Apr 25 '22 07:04 khanhleemtp

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

mattvb91 avatar Aug 20 '22 12:08 mattvb91

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

hugominas avatar Aug 29 '22 16:08 hugominas

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)

tsaunde123 avatar Sep 30 '22 10:09 tsaunde123

@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

jorgegonzalez avatar Apr 26 '23 16:04 jorgegonzalez

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.

diegoddox avatar May 05 '23 09:05 diegoddox