Open DanyRupes opened 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.
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.
The Quill editor runs into similar problems when trying to render static markup. Luckily, React saves the day here with the ReactDOMServer.renderToStaticMarkup() method.
@Enva2712 @jasonadkison Can you guys provide some sample code, please? It will be very helpful for this key common requirement.
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>);
}
@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"}}"`
}
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
<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)
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
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?
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 can you create an example in codesanbox?
i think you forget getDescendants function from where?
@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
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. UsinguseEffect
with React'srenderToStaticMarkup
is a no-op. Here is some code that I use to render static HTMLReplacement
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.
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;
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
I believe this is so trivial that it should be core feature @dbousamra any change we can open a new PR?
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
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
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
I have my project in jsx and when I tried to add this file I face lodash error
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 : (
@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>
`;
return html;
}; `
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?
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
I have exported HTML from JSON. But i don't see js in file. How can do it ?
@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 so you could pass a SSR flag to the same component, to either read the props from useNode or from props?
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 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
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.
I added a POC solution using Next.js https://github.com/reboottime/craftjs-nextjs-ssr-poc after testing some of above ideas.
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
"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>
);
}
It working great, let me know if anybody need any help
@bzaman that's not in plain html format. The discussion topic above was about how to transform the serialized json to plain html IMO
I don't know how to export my page