Kogito Tooling Examples — How to create a custom Editor in a React application
We’re happy to announce our update on the Kogito Tooling Examples to help you understand how to use and integrate the Kogito Tooling packages on your project! The Kogito Tooling packages provide a simple way to start using the Multiplying Architecture. It’s major advantage is enabling you to embed an Editor or a View on your project.
Some useful links before we start:
This will be a series of blog posts, and this one covers “How to create a custom Editor in a React application”.
You can navigate through the series by clicking on the following topics:
- How to create a custom Editor in a React application;
- How to create a Chrome Extension for a custom Editor;
- How to create a VS Code Extension for a custom Editor;
- How to create a custom View;
- How to create a VS Code Extension for a custom View;
- How to create a more complex custom View;
- How to integrate a custom Editor, an existing Editors, and custom Views on a Web App;
Before we start, we need to remember the main concepts of the Multiplying Architecture:
- Channel: The host application can be a Web App, a Chrome Extension, VS Code Extension, Desktop App, etc.
- Envelope: An isolated application running inside an iframe, which can be an Editor or a View.
The Channel and the Envelope communicate by sending messages to each other. These messages can be notifications or requests. A notification is a message without a response (void), and a request implies that a response will be provided, thus returns a Promise. To know more about Channels and Envelopes, see this post.
How to create a custom Editor
You can create a custom Editor using any JavaScript technology you want for any file extension you want. In order for the custom Editor to be compatible with all features provided by Kogito Tooling, the file content must be a text (not binary). This is a major concern if you want to create a Chrome Extension for GitHub using the custom Editor, because at the time this blog post is being written, it’s not possible to edit a binary file on GitHub. (and I don’t think it will ever be 😁)
For this example, we will create an Editor using React for a brand new file extension called base64pn
, which contains a PNG image converted to Base64. The idea is to create an Editor capable of tweaking the image properties but using a different file format.
To start it, we are going to need three important files:
- Base64PngEditorFactory.ts
- Base64PngEditorInterface.tsx
- Base64PngEditor.tsx
Base64Png Editor Factory
This class is utilized on the initialization of the Envelope. It tells how to create a Base64Png Editor Interface (that contains the Base64Png Editor) and which file extensions the factory supports, in our case, just the base64png
On the initialization of the Base64 Editor Interface, one of the parameters is the envelopContext
that gives access to all the services available on the Envelope through the services
property (KeyboardShortcuts, GuidedTour, I18n). It provides a way to communicate with the Channel with the channelApi
property. With the context
property, it’s possible to access the O.S. and Channel (e.g., VS Code) the Envelope is running.
The initArgs
isn’t utilized in this example. Still, with it, you have access to additional useful information, such as the resourcePathPrefix
which is the initial path to where your resources are located (e.g., CSS, icons, fonts). The fileExtension
in case your Editor supports multiple file extensions, this property is initialized with the current one. The initialLocale
a useful property if your Editor supports internationalization (i18n), and an isReadOnly
value if your Editor has a read-only mode.
Base64Png Editor Interface
This class implements an EditorInterface
which determines some methods and properties required by the Channel to communicate with the Editor.
All methods, except the af_componentRoot
are implemented by the Editor and are called using its reference. Some methods can be implemented directly on the interface in case your Editor doesn’t support it.
Base64Png Editor
The Editor itself. Here we will explore some functionalities and some choices that were made and explain the code. Here we’re going to use a lot of React concepts, like Hooks. In case you need a quick reference, here’s the documentation link.
Initial Editor
On this initial setup, we’ll end up with a minimal Base64Png Editor that can only receive a base64 file from the Channel (via setContent
and send it back without any tweak ( getContent
) Here is how it’s going to look like:
We manually set the content with this rainbow image. You can check the code for this section here.
Now we’re going to follow up with some explanations about what is used to create this Editor.
Why RefForwardingComponent
In this example, we used the RefForwardingComponent
available on React to give imperative access to its parent component, which has its reference.
We chose this approach to have access to the React Hooks. To expose the communication methods used by the Base64PngInterface, we utilize the useImperativeHandle
hook.
As you can see in this Gist, all the following code will be inside the RefForwardingComponent
, so it’ll be omited int the next Gists.
What is the receive_ready notification?
Before the Channel starts to send/receive requests to the Envelope, the Editor needs to inform that its ready to do so. To do it, we notify the Channel with a receive_ready
notification after the first render, with a useEffect
hook.
Image and Canvas
States
To have access to the original image and the tweaked image, we need two states. We’re going to use the useState
hook, which provides a state and a setter for the respective state.
- Original Content — The original base64 value
All tweaks are made on top of the original value. This is used because tweaking the image would modify it to another image, and all subsequential tweaks would be applied on top of the last tweak.
- Editor Content — The tweaked base64 value
The editorContent
has the current value of all tweaks that it’s applied to the image. This value is the one used by the canvas to show the tweaked image.
References
To have imperative access to the image and the canvas properties and methods, we must create references, and we use the useRef
hook for this.
HTML/CSS
To start it simple, we’re going to render just the image and the canvas, passing the created references for each one.
Some important considerations:
- The div has the entire screen size.
- The image CSS is a
display:none
because we don’t want the image to be displayed. We just need the image content, so we can use it later to set the canvas content. - The canvas has an arbitrary 600x600 size.
Canvas Content
To set the canvas content, we’re going to use a useEffect
hook and the values on the imageRef
and canvasRef
so when the component is mounted, we can set the initial canvas size and then register a callback on the image, using the onload
property. This callback will be called every time the content on the image is loaded, which is when we have access to the content and size, so we can update the canvas with it. The editorContent
is set with the canvas content after removing the base64
header.
When the component is unmounted, we’re going to remove the registered callback on the onload
property.
setContent/getContent/getPreview
These three methods are made with a useCallback
hook, which will store the functionality without re-evaluating on each render. The setContent
will set the originalContent
state, and getContent
retrieve the editorContent
state. The getPreview
will retrieve an SVG of the current content to be further used as a file image preview.
Adding Tweakers
For this section, we’re going to see how to add an invert image tweaker to an image. It’ll use the filter
property available on the canvas 2d context to apply the invert tweak (you can find more about the available filters here). We’re going to add a disabled state as well, so in case we have an invalid image or no image at all. Here is how the code will look like, and here’s a preview.
Invert Tweaker
Before we start with the tweaker, we’re going to add a disabled
state, whose primary purpose is to disable the possibility to apply tweaks on an invalid image. By default, the Editor starts disabled.
To handle the invert value, we need a new state as well. This state will handle the actual invert value applied to the image. We start it with 0, which is the default value.
We need an useEffect
hook to apply this invert value to the Canvas every time the invert
state is updated. Then, we update the editorContent
state with the current base64 data.
To use the disabled
state, we update the image onload
callback to set the disabled
to false.
Adding PatternFly
With all setup, we’re going to use those new states to show the user when the Editor is disabled, and enable them to tweak the invert value through a switch. To accomplish that, we’re going to use the PatternFly React library, which provides some useful components. To use it on your project, we recommend using the PatternFly React seed example (here).
For this, we wrapped all the content around a div
, which will separate the canvas and the tweaks controller. We’ve added a new component (EmptyState
) to be shown when no image is provided on the canvas. On the tweaks side, we’ve utilized the navigation component (Nav
) and a switch component (Switch
) which will convert the boolean value to 0 (not inverted image) or to 100 (totally inverted image).
Adding State Control and Keyboard Shortcuts
This is our final setup covered on this blog post. We’re going to add a State Control service, so the user will be able to use the undo or redo operations through the Keyboard Shortcuts API. Here is the code for this section.
This is the Base64Png Editor running inside our WebApp example. The image gallery is a custom React component, which interacts if the Editor.
State Control
To keep it simple, we will use the Kogito Tooling State Control implementation available with some modifications. Still, it’s up to you to implement your own State Control service or not. Here we’ve created an interface that represents an Edit and adds two new useful methods to parse the current Edit stored on the stack, representing a Base64PngEdit
and another to clear the state control stack.
To store the State Control instance, we’re going to use an useMemo
hook, which will depend on the originalContent
so the instance will be cleared every time a new file is set. Both undo and redo methods will rely on the Kogito Tooling State Control implementation, and it will be used to update the Editor state with the current Edit. In case the current Edit is undefined (bottom of the stack), the Editor should go back to the initial state.
With the State Control in place, we need to modify some methods to fully access its functionalities.
Firstly we’re going to modify the setContent
method, and now, every time this method is called, it’ll clear the stateControl
instance. This callback should be updated when the stateControl
instance is modified, so we added it to the dependencies list of the hook. This is required because the setContent
method can be called with the current originalContent
and it will not trigger a new instance of State Control on the useMemo
hook.
It’s necessary to update the tweakInvert
method to generate a Base64PngEdit
command. This command will be stored on the stateControl
stack and be notified to the Channel.
To apply the current Edit to the canvas, we update the useEffect
hook responsible for applying the filter property on the canvas context.
Finally, using the State Control isDirty
method, we added an indicator on the end of the NavList
component.
Keyboard Shortcuts
It’s easy to register your shortcuts utilizing the available service on the envelopeContext
. We use a useEffect
hook to register it on the first render, and every time the invert
or the disabled
value is changed to keep the shortcut updated with the latest values. The registerKeyPress
method will return its reference, used to deregister when the component is unmounted.
Wrapping up
With this last setup, we wrap up how to create a custom Editor. Our example repository has this same Editor with a few more tweakers (e.g., contrast, saturation, etc.). If you want to look at the finished Editor, you can check it here.
Check our next session to see how to Embed this Editor on a Chrome Extesnion or on the VS Code!
Thanks for read, and see you in the next section.