B station video demonstration ~
Lot code
React is a simple lowcode platform, as shown in the GIF above. The following is an introduction according to the completed function points, mainly including:
- The editor
- State management
- Custom generated components (currently complete text, button, image components)
- Drag and drop
- Component property editing
- Zoom in and out
- Delete components and adjust layers
- Undo, redo
- animation
- The generator
introduce
Lowcode platform is quite common. At present, there are mature and universal ones made on the Internet, such as Rabbit Exhibition, Yiqixiu, code card, Picture driver, etc. However, for personalized Settings, such as accessing the company’s database, many companies also have their own Lowcode platform, such as Ali, Baidu, Tencent and so on. Lowcode platform is by dragging or click on predefined components to generate pages, more application scenario is the product manager or operations or sales activities from definition generated page, this operation is not too good, because don’t need a lot of product manager, interaction, page design and programmer to complete a meeting activities, This includes setting takedown and expiration time without programmer intervention.
If you haven’t done one yet, let’s go through what it takes to do one.
The project can be divided into two parts, an editor and a generator. The editor is used to generate a page, which actually generates an object value containing information about all the components of the page, as follows:
You can then convert this object into a string and store it in the database, corresponding to an ID.
All the generator has to do is parse the string based on the ID and render the parsed object as a component, which is the page we created in the editor.
Component data structure
As can be seen from the previous figure, there is a CMPS array in the Canvas data, which is all the components.
-
Each component has a randomly generated onlyKey as a unique identifier that can be used to delete, find, update, and so on.
-
Desc and Type identify the type of the component. The former is a Chinese description that can be used for page display, and the latter mainly determines the type of the component.
-
Value is defined differently in different components, such as a text component or button to represent displayed text, and an image component to record the address of the image.
-
Style records the component’s style style
The editor
First, let’s take a look at the layout of the editor, which can be divided into four major modules:
At this point the code looks like this:
export default function App() {
return (
<div id="app" className={styles.main}>{/* Module 1: Component selection */}<Cmps />{/* Modules 2 and 4: Canvas module and canvas manipulation module */}<Content />{/* module 3: Canvas property manipulation module */}<Edit />
</div>
);
}
Copy the code
State management
(For state management, the previous part is my selection process, if you don’t want to see it, you can skip to the last paragraph to see my final choice.)
Now that we know what kind of editor we’re going to build, the next important thing we need to think about is where to put the canvas data? The first thing to know is that when the canvas data changes, the related components are also updated, that is, module 234 is notified of the change, which is called state management.
I have considered the following scenarios for this state management:
- Put the canvas data into the state of the App. Considering the complicated logic of modifying the canvas data, the App function component can use the useReducer.
- The canvas data is managed by REdux, and the reducer function is used to define the canvas data modification rules as in Plan 1.
At the same time, since module 1234 and their children use canvas data, it is time to consider passing data across the hierarchy. Of course, this can use Context, and scenario 12 can use Context.
When I used scheme 1, because the canvas Data was too large, and there were many functions to add, delete, change and check the canvas Data, the App component became bloated, and I felt that View and Data layers were stuck together. It was very difficult to add, delete, change and check functions. Abandon option 1.
Since View and Data of plan 1 are too sticky, I use Redux as a third party to manage canvas Data. At first, the small operation is good, but because it involves the modification of style and value in component Data, the nesting level is a little deep, and many add, delete, change and check functions need to be reused. However, using Reducer, A lot of modification logic needs to be written in the component, but I don’t want the component to be too bloated, and the tool function is separated from View and Data, which is difficult to maintain.
So, in the end, what I really want is a data warehouse to store my canvas data, and this warehouse also provides a lot of functions to add, delete, modify and search.
Finally, I chose the third option, defining a Canvas class myself to manage my Canvas data.
In this class I define the following data:
this.canvas: object
Store all canvas data, namely:
GetCanvas gets this. Canvas data, which is used to publish updates to the database, updateCanvas, which is used to updateCanvas data, emptyCanvas, which is used to emptyCanvas data. There is also updateCanvasStyle to update the style of the canvas.
this.listeners: array
Listen to the function. So if this.canvas changes, what do you do? In this case, this.canvas changes, you just update the whole App. Just add a subscription to the App component:
export default function App() {
const forceUpdate = useForceUpdate();
// All components
const globalCanvas = useCanvas();
useLayoutEffect(() = > {
const unsubscribe = globalCanvas.subscribe(() = > {
forceUpdate();
});
return () = > {
unsubscribe();
};
}, [globalCanvas, forceUpdate]);
return (
<div id="app" className={styles.main}>
<CanvasContext.Provider value={globalCanvas}>
<Cmps />
<Content />
<Edit />
</CanvasContext.Provider>
</div>
);
}
Copy the code
The Canvas subscription function is as follows:
subscribe = (listener) = > {
this.listeners.push(listener);
return () = > {
this.listeners = this.listeners.filter((lis) = >lis ! == listener); }; };Copy the code
Again, although I’ve always stressed that subscribing and unsubscribing must come together.
Of course, if you understand the principles of Redux and Antd4 forms, the logic is very similar.
this.selectedCmp:object
Records the currently selected component. All component data are stored in this.canvas, but because the hierarchy is deep, it is difficult to find the value, so every time you update the selected component and thia. Canvas, you just need to update this value simultaneously.
Enclosing canvasChangeHistory: array and enclosing canvasIndex: number
The former records the canvas modification history and is used for undo and redo in the top module 2. The latter records which change history it is currently in. Each canvas update records the current canvas data, such as updating components, emptying the canvas, etc. The function to record the history of the canvas data modification is as follows:
recordCanvasChangeHistory = () = > {
this.canvasChangeHistory.push(this.canvas);
this.canvasIndex = this.canvasChangeHistory.length - 1; / / 2;
};
Copy the code
Gets the operation function of this class
// Returns a function to add, delete, change, and query canvas data
getCanvas = () = > {
const returnFuncs = [
"getCanvasData"."recordCanvasChangeHistory"."goPrevCanvasHistory"."goNextCanvasHistory"."updateCanvas"."emptyCanvas"."getCanvasStyle"."updateCanvasStyle"."registerStoreChangeCmps"."registerCmpsEntity"."getCmp"."getCmps"."setCmps"."addCmp"."getSelectedCmp"."setSelectedCmp"."updateSelectedCmpStyle"."updateSelectedCmpValue"."deleteSelectedCmp"."changeCmpIndex"."subscribe",];const obj = {};
returnFuncs.forEach((func) = > {
obj[func] = this[func];
});
return obj;
};
Copy the code
Transfer data across hierarchies
Now that the Canvas class is created, the next step is to instantiate the class and pass it through the Context.
export default function App() {
const forceUpdate = useForceUpdate();
// All components
const globalCanvas = useCanvas();
useLayoutEffect(() = > {
const unsubscribe = globalCanvas.subscribe(() = > {
forceUpdate();
});
return () = > {
unsubscribe();
};
}, [globalCanvas, forceUpdate]);
return (
<div id="app" className={styles.main}>
<CanvasContext.Provider value={globalCanvas}>
<Cmps />
<Content />
<Edit />
</CanvasContext.Provider>
</div>
);
}
Copy the code
Instantiate the custom Canvas class in useCanvas and return the getCanvas method.
export function useCanvas(canvas) {
const canvasRef = useRef();
if(! canvasRef.current) {if (canvas) {
canvasRef.current = canvas;
} else {
const globalCanvas = newCanvas(); canvasRef.current = globalCanvas.getCanvas(); }}return canvasRef.current;
}
Copy the code
Custom build components
Module 1 is the part of a custom generated component that needs to be considered for two things:
- Get all custom components and their initial values as follows:
- The second thing is that when adding a component to the canvas, there are two ways:
- Click add component. The default position is in the top left corner of the canvas, where top and left are both 0
- Dragging a component onto the canvas, unlike clicking, requires recording the location of the drag and assigning a value to the style property of the newly added component
Module 1 code is as follows:
export default function Cmps(props) {
const globalCanvas = useContext(CanvasContext);
const [list, setList] = useState(null);
const handleDragStart = (e, cmp) = > {
if (cmp.data.type === isImgComponent) {
return;
}
e.dataTransfer.setData("add-component".JSON.stringify(cmp));
};
const handleClick = (e, cmp) = > {
e.preventDefault();
e.stopPropagation();
if (
cmp.data.type === isTextComponent ||
cmp.data.type === isButtonComponent
) {
globalCanvas.addCmp(cmp);
return;
}
// Image component
if (list) {
setList(null);
} else {
let l = null;
switch (cmp.data.type) {
case isImgComponent:
l = <Img baseCmp={cmp} />;
break;
default:
l = null; } setList(l); }};return (
<div id="cmps" className={styles.main}>
<div className={styles.cmpList}>
{menus.map((item) => (
<div
key={item.desc}
className={styles.cmp}
draggable={item.data.type! = =isImgComponent}
onDragStart={(e)= > handleDragStart(e, item)}
onClick={(e) => handleClick(e, item)}>
{item.desc}
</div>
))}
</div>
{list && (
<button
className={classnames("iconfont icon-close", styles.close)}
onClick={()= > setList(null)}></button>
)}
{list && <ul className={styles.detailList}> {list}</ul>}
</div>
);
}
Copy the code
Component property editing
After adding a component to the canvas, the component is selected by default, and the editing module on the right needs to display the properties of the component and be editable.
Drag and drop components
The component on the canvas needs to be draggable. By dragging and dropping, you control the position. In this case, you get the distance between the x and y axes, so you just subtract the initial position from this position. Note also that the component needs to be updated frequently because drag and drop frequently modify the canvas data and because of the function that was set up to listen on, but this does not need to update the component every time it is moved. You can improve performance by throttling, such as updating every 500ms. The event code is as follows:
Record the initial position:
handleDragStart = (e) = > {
this.setActive(e);
let pageX = e.pageX;
let pageY = e.pageY;
e.dataTransfer.setData("startPos".JSON.stringify({pageX, pageY}));
};
Copy the code
The drop event on the canvas, which needs to determine whether the new component is added or the existing component is dragged and changed,
const handleDrop = (e) = > {
e.preventDefault();
e.stopPropagation();
// The new component
let addingCmp = e.dataTransfer.getData("add-component");
if (addingCmp) {
// Drag and drop the newly added component
addingCmp = JSON.parse(addingCmp);
const top = e.pageY - canvasPos.top - 15;
const left = e.pageX - canvasPos.left - 40;
letresData = { ... addingCmp,data: {
...addingCmp.data,
style: {
...addingCmp.data.style,
top,
left,
},
},
};
globalCanvas.addCmp(resData);
} else {
// Drag and drop components inside the canvas
let startPos = e.dataTransfer.getData("startPos");
startPos = JSON.parse(startPos);
let disX = e.pageX - startPos.pageX;
let disY = e.pageY - startPos.pageY;
// Get the latest information about the currently selected component
const selectedCmp = globalCanvas.getSelectedCmp();
const top = selectedCmp.data.style.top + disY;
constleft = selectedCmp.data.style.left + disX; globalCanvas.updateSelectedCmpStyle({top, left}); }};Copy the code
Zoom in and out
The components on the canvas should also be able to zoom in and out in eight directions, similar to dragging and dropping. Just record the mouse movement distance, and then change width, height, top, and left. Note that the components are positioned according to top and left. So there is no need to change top and left when going down, right and right, because the top left coordinate is not changed, the event code is as follows:
handleMouseDown = (e, direction) = > {
e.stopPropagation();
e.preventDefault();
const cmp = this.context.getCmp(this.props.index);
let startX = e.pageX;
let startY = e.pageY;
const move = (e) = > {
let x = e.pageX;
let y = e.pageY;
let disX = x - startX;
let disY = y - startY;
let newStyle = {};
if (direction) {
if (direction.indexOf("top") > =0) {
disY = 0 - disY;
newStyle.top = cmp.data.style.top - disY;
}
if (direction.indexOf("left") > =0) {
disX = 0- disX; newStyle.left = cmp.data.style.left - disX; }}// Especially frequent changes, add a mark,
debounce(
this.context.updateSelectedCmpStyle( { ... newStyle,width: cmp.data.style.width + disX,
height: cmp.data.style.height + disY,
},
"frequently")); };const up = () = > {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
this.context.recordCanvasChangeHistory();
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
};
Copy the code
rotating
The rotate component is similar to drag and drop. It records the x and Y axis distance of the mouse movement and calculates the mouse movement Angle. The rotate value of the component’s transform can be updated. The code is as follows:
handleMouseDownofRotate = (e) = > {
e.stopPropagation();
e.preventDefault();
const {getCmp, updateSelectedCmpStyle} = this.context;
const cmp = getCmp(this.props.index);
let startX = e.pageX;
let startY = e.pageY;
const move = (e) = > {
let x = e.pageX;
let y = e.pageY;
let disX = x - startX;
let disY = y - startY;
const deg = (360 * Math.atan2(disY, disX)) / (2 * Math.PI);
// Especially frequent changes, add a mark,
debounce(
updateSelectedCmpStyle(
{
transform: `rotate(${deg}deg)`,},"frequently")); };const up = () = > {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
this.context.recordCanvasChangeHistory();
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
};
Copy the code
Right-click menu
Select a component and right-click to present a menu that shows how to copy, delete, top, and display all components. The display of this component can be judged by a status value. If you select the component, right click to display it, and click other areas to hide the menu:
{showContextMenu && (
<ContextMenu
index={index}
pos={{top: style.top - 80.left: style.left + 60}}
cmp={cmp}
/>
)}
Copy the code
copy
Copying is simply copying a copy of the selected component’s data and adding it.
const copy = () = > {
globalCanvas.addCmp(cmp);
};
Copy the code
Note that onlyKey needs to be generated again, and the selected component needs to be updated as the newly copied component:
addCmp = (_cmp) = > {
this.selectedCmp = { ... _cmp,onlyKey: getOnlyKey(),
};
const cmps = this.getCmps();
this.updateCmps([...cmps, this.selectedCmp]);
};
Copy the code
delete
SeletedCmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp = this.seletedcmp Then update the canvas and edit area components.
// Click the component and right-click to delete the component
deleteSelectedCmp = (_cmp) = > {
this.setSelectedCmp(null);
const cmps = this.getCmps();
this.updateCmps(cmps.filter((cmp) = >cmp.onlyKey ! == _cmp.onlyKey)); };Copy the code
Top and bottom
The hierarchy of all components is controlled by z-index, which is the subscript of the component in the CMPS array, so the hierarchy can be adjusted by updating the order of the components in the array. The top swap the position of the last component in the CMPS with the selected component, and the bottom swap the position of the 0th component in the CMPS with the selected component.
const beTop = (e) = > {
globalCanvas.changeCmpIndex(index);
};
const beBottom = (e) = > {
globalCanvas.changeCmpIndex(index, 0);
};
Copy the code
Show all components
Right-click menu and a function is to show all of the components, because too many components, some components will be covered, but can not be selected from the canvas be override components, this time can appear by right of the menu to view all of the components, the mouse stay, will display the corresponding components, click on the word has the function of the selected. The event code is as follows:
const cmps = globalCanvas.getCmps();
const mouseOver = (e, _cmp) = > {
let cmpTarget = document.getElementById("cmp" + _cmp.onlyKey);
let prevClassName = cmpTarget.className;
if (prevClassName.indexOf("hover") = = = -1) {
cmpTarget.setAttribute("class", prevClassName + " hover"); }};const mouseLeave = (e, _cmp) = > {
let cmpTarget = document.getElementById("cmp" + _cmp.onlyKey);
let prevClassName = cmpTarget.className;
if (prevClassName.indexOf("hover") > -1) {
cmpTarget.setAttribute("class", prevClassName.slice(0, -6)); }};const selectCmp = (e, cmp) = > {
globalCanvas.setSelectedCmp(cmp);
};
Copy the code
Step up, step up
It’s basically a time machine, and in order to go back in time, you need to record your revision history. We need two values this.canvasChangeHistory and this.canvasIndex. When altering the data of the canvas, and component data to perform this. RecordCanvasChangeHistory () function to record history. Click on the previous step to get the previous value of this.canvasIndex in this.canvasChangeHistory, and the next step gets the next value. Notice that the 0th and last check boundary values are ok.
animation
The component allows you to add animation properties. Here, three animations are copied from the rabbit show, and you can change the duration of an animation, the number of repetitions, and the delay time.
Select a template
It’s too slow to create from zero each time. You can just make some preset templates and fill the canvas with data.
export default function Tpl({openOrCloseTpl, globalCanvas}) {
const updateCmps = (cmps) = > {
globalCanvas.updateCanvas(JSON.parse(cmps));
openOrCloseTpl(false);
};
return (
<ul className={styles.main}>
<li className={styles.close} onClick={openOrCloseTpl}>
<i className="iconfont icon-close"></i>
</li>
{tplData.map((item) => (
<li
key={item.id}
className={styles.item}
onClick={()= > updateCmps(item.cmps)}>
<div className={styles.name}>{item.name}</div>
<div className={styles.thumbnail}>
<img src={item.img} />
</div>
</li>
))}
</ul>
);
}
Copy the code
The generator
Once you’re done with the editor, you can take the canvas out and do another generator project,
function App() {
const [canvas, setCanvas] = useState(null);
const {cmps, style} = canvas || {};
useEffect(() = > {
let cc = JSON.parse(
/ /! Lantern Festival
'{"style":{"width":320,"height":568,"backgroundColor":"#fc0000ff","backgroundImage":"https://img.tusij.com/ips_asset/16/ 11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png! l800_i_w? auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094a1516003","backgroundPosition":"center","backgroundSize":"cover","backgr OundRepeat ":" no - repeat ", "boxSizing" : "the content - box"}, "CMPS" : [{" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ ips_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc80 94a1516003","style":{"top":-1,"left":-1,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth": "0" and "borderColor", "# FFF"}}, "onlyKey" : 0.27364639468523455}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/i ps_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc809 4a1516003","style":{"top":155,"left":-3,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth": "0" and "borderColor", "# FFF"}}, "onlyKey" : 0.7545885469950053}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ip s_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094 a1516003","style":{"top":420,"left":-3,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth":" 0 "and" borderColor ":" # FFF "}}, "onlyKey" : 0.7590306166672274}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips _asset/16/11/10/44/53/ca/ca7ebd1a9683109e61f374e75e87fc85.png!l800_i_w?auth_key=1639324800-0-0-04d5239353f80379a2430dc74 d1ac11a","style":{"top":18,"left":211,"width":89,"height":81,"borderRadius":"0%","borderStyle":"none","borderWidth":"0", "BorderColor" : "# FFF"}}, "onlyKey" : 0.14191580299167428}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips_a sset/16/11/10/44/54/70/70913bd41742596a4a0dd68b088e6551.png!l800_i_w?auth_key=1639324800-0-0-2a8cd9567a9d2a9aa2ddd8acc4a 24450","style":{"top":460,"left":0,"width":320,"height":110,"borderRadius":"0%","borderStyle":"none","borderWidth":"0"," BorderColor ":" # FFF "}}, "onlyKey" : 0.5399342806341869}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips_ass et/16/11/10/44/53/9a/9a353760e02b49cbdd2706f5c452291b.png!l800_i_w?auth_key=1639324800-0-0-8825104eb9f4bd5ca42b9ff8c3690 c9c","style":{"top":403,"left":10,"width":121,"height":50,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","bo RderColor ":" # FFF "}}, "onlyKey" : 0.27065004352847866}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips_asse t/16/11/10/44/53/69/6917ec339fa98e4cb97cf596cc9179df.png!l800_i_w?auth_key=1639324800-0-0-31958bfca526c4f4f87f4363b8b16b 61","style":{"top":461,"left":28,"width":97,"height":49,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","bord ErColor ":" # FFF "}}, "onlyKey" : 0.3396974553981347}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips_asset/1 6/11/10/44/53/e7/e722646ec5596c852c8b193b2ef09db9.png!l800_i_w?auth_key=1639324800-0-0-0e5dcd8e08ad1e7f0de72c2dad23419c" ,"style":{"top":439,"left":158,"width":100,"height":47,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borde RColor ":" # FFF "}}, "onlyKey" : 0.02766075271613433}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusij.com/ips_asset/1 6/11/10/44/54/09/09917bf7e35711c91d353fd7aebf2a38.png!l800_i_w?auth_key=1639324800-0-0-bd838424e74c24b3f0787ae4c4fb11d6" ,"style":{"top":215,"left":116,"width":114,"height":154,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","bord ErColor ":" # FFF "}}, "onlyKey" : 0.6929555070607207}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://tva1.sinaimg.cn/large/008 eGmZEly1gnqdhx1eprj303m03mjrm.jpg","style":{"top":388,"left":245,"width":41,"height":58,"borderRadius":"0%","borderStyle ":"none","borderWidth":"0","borderColor":"#fff","animationName":"wobble","animationDelay":0,"animationDuration":"8","ani MationIterationCount ":" infinite "}}, "onlyKey" : 0.7708575276016363}, {" desc ":" image ", "data" : {" type ": 2," value ":" https://img.tusi j.com/ips_asset/15/48/39/56/74/56/564896077cb72510ff3b920732d8c53c.png!l800_i_w?auth_key=1639152000-0-0-456d31b72cda757a e3945425296bd646","style":{"top":173,"left":248,"width":51,"height":58,"borderRadius":"0%","borderStyle":"none","borderW Idth ":" 0 "and" borderColor ":" # FFF "}}, "onlyKey]" : 0.540328523257599}} '); setCanvas(cc); } []);return canvas ? (
<div
className={styles.main}
style={{
. formatStyle(style),
backgroundImage: `url(${style.backgroundImage}) `,}} >
{cmps.map((cmp, index) => (
<Draggable
key={cmp.onlyKey}
cmp={cmp}
index={index}
canvasWidth={style.width}
canvasHeight={style.height}
/>
))}
</div>
) : (
<div>
<i className="iconfont icon-loading"></i>
</div>
);
}
Copy the code
The end ~
Don’t forget to give the article a thumbs up and welcome to pay attention to the public account [Huaguoshan front end] and my B station. More original videos will be uploaded later