Craft.js Documentation
repository·main·Indexed 27 days ago
https://github.com/prevwong/craft.jsA modular React framework for building extensible page editors. Craft.js provides core building blocks for drag-and-drop, component rendering, and state management, allowing developers to create custom user interfaces. It includes @craftjs/core for editor logic, @craftjs/layers for a layers panel, and @craftjs/slate for integrating Slate.js rich text editing capabilities.
What's inside Craft.js
- @craftjs/slate is a package designed to help you create Rich Text Editor (RTE) User Components using Slate.js within the Craft.js ecosystem. It bridges the gap between Craft.js's component-based drag-and-drop architecture and Slate.js's specialized text editing capabilities.
Understand User Elements and Nodes in Craft.js
mainIn Craft.js, User Elements are the React elements (HTML tags or React Components) that users manipulate, drag, or drop within the editor.
Craft.js manages these elements using an internal state of Nodes. Every User Element is represented by a corresponding Node that stores metadata such as element type, current props, the DOM element, and parent/child relationships.
Configure Custom Properties on Nodes
mainYou can attach custom data to nodes using the
customproperty. This is useful for storing metadata that needs to be persisted (e.g., in a database) but isn't part of the standard React props.You can define default custom values on a component using the
craft.customproperty.Make a component editable with useNode
mainUse the
useNodehook to connect your React components to the Craft.js editor. To make a component interactive (e.g., for drag-and-drop or selection), use theconnectorsreturned byuseNode.To implement custom editing logic (like a property editor modal), you can pass a selector function to
useNodeto subscribe to specific editor states, such asstate.events.selected. You can then useactions.setPropto update the component's props.import { useNode } from "@craftjs/core"; const TextComponent = ({ text }) => { const { connectors: { connect, drag }, isClicked, actions: { setProp }, } = useNode((state) => ({ isClicked: state.events.selected, })); return ( <div ref={(dom) => connect(drag(dom))}> <h2>{text}</h2> {isClicked ? ( <Modal> <input type="text" value={text} onChange={(e) => setProp(e.target.value)} /> </Modal> ) : null} </div> ); };Implement Related Components for Toolbars
mainTo build a UI (like a Toolbar) that edits a selected component, use the
relatedproperty inText.craftto link a settings component to your user component.Inside the related component,
useNodewill access the same Node as the user component. In the main editor, you can retrieve the related component from theuseEditorhook by accessingstate.nodes[selectedNodeId].related[key].Serialize and restore editor state
mainCraft.js allows you to save the entire editor state as a JSON string, which can be stored in a database and later used to reconstruct the editor.
- To Save: Call
query.serialize()from theuseEditorhook. - To Load: Pass the retrieved JSON string to the
jsonprop of the<Frame />component.
// To Save const SaveButton = () => { const { query } = useEditor(); return <a onClick={() => console.log(query.serialize()) }>Get JSON</a> } // To Load const App = () => { const jsonString = /* retrieve JSON from server */ return ( <Editor> <Frame json={jsonString}> ... </Frame> </Editor> ) };- To Save: Call
Migrate EditorState events from 0.1.x to 0.2.x
mainIn version 0.2.x, the
eventsproperty inEditorStatewas changed from a singleNodeIdto aSet<NodeId>to support Multiselect. When accessingselected,hovered, ordraggedvia theuseEditorhook, you must now use the.has()method instead of direct equality checks.// 0.1.x const { selected, hovered, dragged } = useEditor(state => ({ selected: state.events.selected === 'some-node-id', hovered: state.events.hovered === 'some-node-id', dragged: state.events.dragged === 'some-node-id', })) // 0.2.x const { selected, hovered, dragged } = useEditor(state => ({ selected: state.events.selected.has('some-node-id'), hovered: state.events.hovered.has('some-node-id'), dragged: state.events.dragged.has('some-node-id') }))Configure Nodes inside <Frame />
mainBecause the
<Frame />component automatically creates a Node for all of its children, you can use<Element />inside a<Frame />to simply configure the values of those automatically created Nodes (e.g., making them droppable or setting custom properties).import { Craft, Frame, Element } from "@craftjs/core"; const App = () => { return ( <div> <h2>My App!</h2> <Craft resolver={{ MyComp }}> <h2>My Page Editor</h2> <Frame> {/* defines the Root Node, droppable */} <Element is="div" canvas> <h2>Drag me around</h2> <MyComp text="You can drag me around too" /> {/* Canvas Node of type div, draggable and droppable */} <Element is="div" style={{ background: "#333" }} canvas> <p>Same here</p> </Element> </Element> </Frame> </Craft> </div> ); };Define Linked Nodes inside User Components
mainWhen using
<Element />inside a User Component, there is no Node in-place by default. You must use<Element />to create a 'Linked Node'—a Node that is linked to the Node of the containing User Component via an arbitraryid.Note:
<Element />used inside a User Component MUST specify anidprop.Create droppable regions with Canvas
mainTo allow users to drag and drop components into a specific area of a component (creating a container), wrap the target area in a
<Canvas />component. This turns that region into a valid drop zone within the editor.import {useNode} from "@craftjs/core"; const Container = () => { const { connectors: {drag} } = useNode(); return ( <div ref={drag}> <Canvas id="drop_section"> {/* Now users will be able to drag/drop components into this section */} <TextComponent /> </Canvas> </div> ) }Install @craftjs/core
mainTo use Craft.js in your project, install the core package using yarn or npm.
yarn add @craftjs/coreor
npm install --save @craftjs/coreSerialize and compress editor state
mainTo save the editor state, use
query.serialize()to obtain the JSON representation of the nodes. For efficient storage or transfer, it is recommended to compress this JSON using a library likelzutf8and encode it to base64.To copy the compressed state to the clipboard, you can combine
query.serialize(),lz.compress(), andlz.encodeBase64()with a clipboard utility likecopy-to-clipboard.// Example: Copying compressed state to clipboard const json = query.serialize(); copy(lz.encodeBase64(lz.compress(json)));