This is the repo for Phase Frontend Test: Editor. This challenge provides the basic layout of the editor at here.
- Version of react, react-dom and react-scripts are updated to lastest version
- Use ReactPixi to stimulate the canvas section
- Use Recoil to manage states
- version of styled-components is downgraded to 5.0.0 to avoid the issue of Invalid Hook Call Warning.
- Implement Page switching
- Implement Element selection
- in Element section in LeftPanel
- in Canvas
- Update the Element based on property changes in RightPanel
- Changing X or Y values should change displayed position of the selected Element on the Canvas
- Changing color or opacity should visually change color or transparency of the selected Element on the Canvas
- Implement moving / dragging Elements on the Canvas (drag-and-drop)
- Implement double-click to rename for Elements in Element list section and Pages in Page list section
- (Optional) Implement nested Element list, i.e. where elements can contain other elements.
- Unit tests
- Have no performance issue (Hopefully)
Note: This project use ReactPixi, which is using Pixi.js under the hood. Please make sure that your browser support WebGL.
# install dependencies
yarn install
# run the project
yarn startYou can view the project at http://localhost:3000 after starting up.
The project src is mainly divided into 3 sections: LeftPanel, Canvas and RightPanel. The LeftPanel and RightPanel are implemented by React, while the Canvas section is stimulated with ReactPixi.
While components, test and recoil are the folders for components, test files and recoil states respectively. Components folder is used to store the components that might be used in multiple places. But this project is not that complicated, so the components in the folder are used very few times.
src
├── leftPanel
├── canvas
├── rightPanel
│
├── components
├── recoil
└── test
Recoil is used to manage states in this project. The resaon for using Recoil is that it store states independently, which is atom, and are units of state in Recoil. Unlike Redux, any changes in any atom will not affect other atoms, therefore preventing unnecessary re-render for un-affected React components.
The whole document is assumed to be a tree structure, which is a nested object. The root of the tree is the document, which contains multiple pages. Each page contains multiple elements. Each element can contain multiple elements as its children.
Therefore, there will be three main type of data structure: document, page and element. The data structure is defined as below:
{
type: 'document',
id: string,
pagesId: [string],
...otherDocumentProperties such as name, backgroundColor, etc.
}{
type: 'page',
id: string,
name: string,
documentId: string,
children: [string], // array of element id
}{
type: 'frame', // could be svg, png, text, etc.
id: string,
name: string,
position: { x: number, y: number },
size: { width: number, height: number },
parentId: string, // id of the parent element, could be page id or another element id
childrenIds: [string], // array of id of children elements
...otherElementProperties such as fill, o, border, etc.
}Nested Elements can be stimulated in canvas, here are the features:
- children's position is relative to their parent
- draging parent element will also drag its children, but draging children will not affect their parent
but there are some limitations:
- dragging children outside of their parent will not make them become an element independent from their parent
- elements layering is not supported, which means that the elements rendered later will be on top of the elements rendered earlier
During the implementation, I am not able to bypass the Recoil states into children of Stage component, which is the root container of Pixi.js when using the package ReactPixi. Althought official docs shows the example of passing context of Redux into it, I couldn't make it work with Recoil.
So i use the package recoil-nexus to get the states from Recoil in the children of Stage Container, let's say 'Element' component.
However, it is a single-time getter, it wouldn't be triggered when states are changed. So i have to use useTick hook provided by ReactPixi, which when ticker event is fired in each Element Sprite, it will fetch the states from Recoil again and update the local state in Element component.
As can be expected, this will affect the performance of the canvas section. But I come up with an idea to address this issue:
- Use Pixi.js directly instead of ReactPixi.
- Keep using React components to represent their Pixi object in canvas, and not returning/rendering DOM elements. This will be easier for states management.
- Then in each Elements' React component, update the respective Pixi object they representing when states are changed, which is like React components usually do by re-rendering DOM elements.
I thought that ReactPixi already handle the states change as above, but I realized that it is not the case only when I start implementing the canvas section with it. I could have use the method stated above from the beginning, but I already spend too much time on ReactPixi, so I decided to keep using it in this project.
- Switch to TypeScript (I am rushing at the start and forgot sbout it...)
- remove reactPixi, use pixi.js directly and use the method stated above to address the performance issue
- create Recoil states in run time, so that the states can be created dynamically, but this require backend database to support this features
- use Recoil's selector to create functions that get the properties, name, selection state and renaming state from atom, so that update one of them wont trigger the other, so that related component wont be re-rendered, further improving the performance
- when key in new properties value at right panel, unselect the element by clicking blank area at left panel will not save the new properties value
- resizing the loaded web page won't responsively resize Pixi Canvas, but will adjust size only after refreshing (this is because size of Stage can't be dynamically changed)