In this tutorial, we'll be designing a simple page editor. It's recommended that you have a basic to intermediate workings of React and it'd be even better if you first have a quick glance at the Core Concepts and come back here. If you are feeling adventurous, that's fine too.
or with npm:
With Craft.js you decide how your editor should look and function. So, let's build a user interface for our page editor. We'll add the page editor functionalities later.
To make our lives easier, we'll use some external packages for designing our user interfaces.
Let's first create the User Components - the components that our end users will be able create/edit/move around.
We will also create a Container component to allow our users to change its background colour and padding.
Now, let's create another user component that will be more advanced. It will be composed of the Container component we made earlier, and it will contain two droppable regions; one for text and another for buttons.
Let's build a "toolbox" which our users will be able to drag and drop to create new instances of those User Components we just defined.
We also want to create a section here where we can display a bunch of settings which our users can use to edit the props of the user components.
For now, let's just put in some dummy text fields. We'll revisit this in the later sections.
Let's design a section that is going to contain a switch for users to disable the editor's functionality and also a button that is simply going to display the serialized output in the browser's console.
Now, let's put together our entire React application.
Up to this point, we have made a user interface for our page editor. Now, let's get it to work!
- First wrap our application with
<Editor />which sets up the Editor's context. We'll also need to specify the list of user components in the
resolverprop for Craft.js to be able to (de)serialize our User Components.
- Then wrap the editable area with
<Frame />which passes the rendering process to Craft.js.
Every element that is rendered in
<Frame /> is managed by an object in the editor's internal state called a
Node which describes the element, its events, and props among other things.
Whether an element is draggable or droppable (or neither) depends on the type of
Node that manages it.
- If the
Nodeis a Canvas, then it's droppable
- If the
Nodeis an immediate child of a Canvas, then it's draggable.
By default, every element inside the
<Frame /> will have a non-Canvas Node automatically defined for it:
Hence, by default, all the Nodes above are neither draggable nor droppable. So how can we define some of the Nodes above as a Canvas Node?
We can use the provided
<Element /> component to manually define Nodes:
In the above code, we've wrapped our
Container components with
<Element /> with the
canvas prop, thus making the component droppable and its immediate children, draggable.
Once you've applied these changes and refresh the page, you will notice that absolutely nothing has changed - and that's a good thing!
Inside a User Component, we have access to the
useNode hook which provides several information and methods related to the corresponding
The first thing we will need to do is to let Craft.js to manage the DOM of our component. The hook provides
connectors which act as a bridge between the DOM and the events in Craft.js:
Let's break this down a little:
- We passed the
connectconnector to the root element of our component; this tells Craft.js that this element represents the Text component. If the component's corresponding Node is a Canvas, then this also defines the area that is droppable.
- Then, we also passed
dragconnector to the same root element; this adds the drag handlers to the DOM. If the component's Node is a child of a Canvas, then the user will be able to drag this element and it will move the entire Text component.
We can also specify additional configuration to our component via the
craft prop. Let's define drag-n-drop rules for our Text Component:
Our Text component can now only be dragged if the
text prop is not set to "Drag" 🤪
Nice, now let's enable drag-n-drop for the other User Components:
At this point, you could refresh the page and you would be able to drag stuff around.
Of course, our Card component is supposed to have 2 droppable regions, which means we'll need 2 Canvas nodes.
But hold up, how do we even create a Node inside a User Component? Remember the
<Element /> component that was used to define Nodes earlier in our application? Well it can be used here as well.
<Element />used inside User Component must specify an
You might be wondering how do we set drag/drop rules for the new droppable regions we made. Currently, we have set the
is prop in our
<Element /> to a div, but we can actually point it to a User Component.
Hence, we can specify and create a new User Component and define rules via the
craft prop just like what we have done previously.
Remember that every User Component must be added to our resolver, so let's add CardTop and CardBottom:
Let's go back to our Toolbox component and modify it so that dragging those buttons into the editor will create new instances of the user components they represent. Just as
useNode provides methods and information related to a specific
useEditor specifies methods and information related to the entire editor's state.
useEditor also provides
connectors; the one we are interested in right now is
create which attaches a drag handler to the DOM specified in its first argument and creates the element specified in its second arguement.
Notice for our Container component, we wrapped it with the
<Element canvas /> - this will allow our users to drag and drop a new Container component that is droppable.
Now, you can drag and drop the Buttons, and they will actually create new instances of our User Components.
Up until this point, we have a page editor where our users can move elements around. But, we are missing one important thing - enabling our users to edit the components' props.
useNode hook provides us with the method
setProp which can be used to manipulate a component's props. Let's implement a content editable for our Text Component:
For simplicity's sake, we will be using
But let's only enable content editable only when the component is clicked when it's already selected; a double click is essential.
useNode hook accepts a collector function which can be used to retrieve state information related to the corresponding
This should give you an idea of the possibilities of implementing powerful visual editing features like what you'd see in most modern page editors.
While we are at it, let's also add a slider for users to edit the
We can agree that it does not look all that good since it obstructs the user experience. Wouldn't it be better if the entire
.text-additional-settings Grid is relocated to the Settings Panel that we created earlier?
The question is, how will the Settings Panel be able render the
.text-additional-settings when our Text component is selected?
This is where Related Components become useful. Essentially, a Related Component shares the same
Node context as our actual User component; it can make use of the
useNode hook. Additionally, a Related Component is registered to a component's
Node, which means we can access and render this component anywhere within the editor.
Before we move on to the Settings Panel, let's quickly do the same for the other User Components:
Setting default props is not strictly necessary. However, it is helpful if we wish to access the component's props via its corresponding
Node, like what we did in the
settings related component above.
For instance, if a Text component is rendered as
<Text text="Hi" />, we would get a null value when we try to retrieve the
fontSize prop via its
Node. An easy way to solve this is to explicity define each User Component's
We need to get the currently selected component which can be obtained from the editor's internal state. Similar to
useNode, a collector function can be specified to
useEditor. The difference is here, we'll be dealing with the editor's internal state rather than with a specific
Note: state.events.selected is of type
Set<string>. This is because in the case of multi-select, it's possible for the user to select multiple Nodes by holding down the
Now, let's replace the placeholder text fields in our Settings Panel with the
settings Related Component:
Now, we have to make our Delete button work. We can achieve this by using the
delete action available from the
Also, it's important to note that not all nodes are deletable - if we try to delete an undeletable Node, it'll result in an error. Hence, it's good to make use of the helper methods which helps describe a Node. In our case, we would like to know if the currently selected Node is deletable before actually displaying the "Delete" button. We can access the helper methods via the
node query in the
This is the last part of the editor that we have to take care of and then we're done!
First, we can get the editor's
enabled state by passing in a collector function just like what we did before. Then, we can use the
setOptions action to toggle the
useEditor hook also provides
query methods which provide information based the editor'state. In our case, we would like to get the current state of all the
Nodes in a serialized form; we can do this by calling the
serialize query method.
We'll explore how to compress the JSON output and have the editor load from the serialised JSON in the Save and Load guide.
We've made it to the end! Not too bad right? Hopefully, you're able to see the simplicity of building a fully working page editor with Craft.js.
We do not need to worry about implementing the drag-n-drop system but rather simply focus on writing rules and attaching connectors to the desired elements.
When it comes to writing the components themselves, it is the same as writing any other React component - you control how the components react to different editor events and how they are edited.