With the development of front-end technology, componentization is becoming more and more mature. As a front-end, our daily work is to stack pages with components. Is there a way like CocosCreator to realize our pages and functions by binding components and scripts? Today we are going to implement a productivity improvement tool visual drag page editor, let products and UI through drag and drop edit page, produce their desired page.

The technical framework uses Vue3 + Typescript + ElementPlus

Commit codes are posted below each chapter for comparison

The final result

Functions:

  • Home page structure: list of optional components on the left, container canvas in the middle, properties defined by editing components on the right
  • Drag and drop components from the menu to the container;
  • Single selection, multiple selection;
  • Components within the container can be dragged and moved;
  • Component drag adjust width and height;
  • Component drag edge, display auxiliary line;
  • Action bar buttons and commands
    • Undo, redo;
    • Import, export;
    • Top and bottom;
    • Delete, empty;
  • Component binding value;
  • Customize the behavior of a component through the scope slot based on the component identifier

Preview the address

I. Project construction and page layout

Generate projects through vuE-CLI

vue create visual-editor-vue
Copy the code
  • Choose Manual Configuration

Select the following configuration:

  • Select the vue3.x version

  • In this step, choose Y to write components using JSX and add the corresponding Babel plug-in

Let’s implement the basic left, middle and right layout

  • The left menu bar places the list of components
  • In the middle are the canvas and toolbar for editing the preview page
  • On the right are the properties of a component that are displayed when we select it

The first part code:Basic layout

Second, data structure design and bidirectional binding

Data structure design

  • Define the data structure as follows
    • Container indicates the canvas container
    • Blocks represent components placed in a container
    • Each block represents a component and contains information such as its type location, width and height, and selection status
  • The canvas uses absolute positioning, where elements are positioned by top and left
{
  "container": { 
    "height": 500."width": 800
  },
  "blocks": [{"componentKey": "button"."top": 102."left": 136."adjustPosition": false."focus": false."zIndex": 0."width": 0."height": 0
    },
    {
      "componentKey": "input"."top": 148."left": 358."adjustPosition": false."focus": false."zIndex": 0."width": 244."height": 0}}]Copy the code

Data bidirectional binding implementation

  • The component is written with JSX syntax in VUe3. It needs to realize bidirectional data binding mechanism. UseModel is used to deal with bidirectional data binding
import { computed, defineComponent, ref, watch } from "vue";

// Implement bidirectional data binding when encapsulating components with JSX
export function useModel<T> (getter: () => T, emitter: (val: T) => void) {
  const state = ref(getter()) as { value: T };

  watch(getter, (val) = > {
    if (val !== state.value) {
      state.value = val;
    }
  });

  return {
    get value() {
      return state.value;
    },
    set value(val: T) {
      if(state.value ! == val) { state.value = val; emitter(val); }}}; }Copy the code

UseModel usage

// External modelValue can be bound with v-model
export const TestUseModel = defineComponent({
  props: {
    modelValue: { type: String}},emits: {
    "update:modelValue": (val? :string) = > true,},setup(props, ctx) {
    const model = useModel(
      () = > props.modelValue,
      (val) = > ctx.emit("update:modelValue", val)
    );
    return () = > (
      <div>Custom input box<input type="text" v-model={model.value} />
      </div>); }});Copy the code

Part II Code

Block rendering

  • Create a component for visual-Editor-block
  • Block to represent the component elements displayed on the canvas
  • Blocks are first displayed as text
import { computed, defineComponent, PropType } from "vue";
import { VisualEditorBlockData } from "./visual-editor.utils";

export const VisualEditorBlock = defineComponent({
  props: {
    block: {
      type: Object as PropType<VisualEditorBlockData>,
    },
  },
  setup(props) {
    const styles = computed(() = > ({
      top: `${props.block? .top}px`.left: `${props.block? .left}px`,}));return () = > (
      <div class="visual-editor-block" style={styles.value}>This is a block</div>); }});Copy the code
  • Pass the defined data into the editor using the V-Model

App. Vue file

<template>
  <div class="app">
    <visual-editor v-model="editorData" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { VisualEditor } from ".. /src/packages/visual-editor";

export default defineComponent({
  name: "App".components: { VisualEditor },
  data() {
    return {
      editorData: {
        container: {
          height: 500.width: 800,},blocks: [{top: 100.left: 100 },
          { top: 200.left: 200},],}}; }});</script>
Copy the code
  • Import the block component and render it

Visual – editor. TSX composite file

import { computed, defineComponent, PropType } from "vue";
import { useModel } from "./utils/useModel";
import { VisualEditorBlock } from "./visual-editor-block";
import "./visual-editor.scss";
import { VisualEditorModelValue } from "./visual-editor.utils";

export const VisualEditor = defineComponent({
  props: {
    modelValue: {
      type: Object as PropType<VisualEditorModelValue>,
    },
  },
  emits: {
    "update:modelValue": (val? : VisualEditorModelValue) = > true,},setup(props, ctx) {
    const dataModel = useModel(
      () = > props.modelValue,
      (val) = > ctx.emit("update:modelValue", val)
    );
    const containerStyles = computed(() = > ({
      width: `${props.modelValue? .container.width}px`.height: `${props.modelValue? .container.height}px`,}));return () = > (
      <div class="visual-editor">
        <div class="menu">menu</div>
        <div class="head">head</div>
        <div class="operator">operator</div>
        <div class="body">
          <div class="content">
            <div class="container" style={containerStyles.value}>{(dataModel.value? .blocks || []).map((block, index: number) => (<VisualEditorBlock block={block} key={index} />
              ))}
            </div>
          </div>
        </div>
      </div>); }});Copy the code
  • The final result

  • The canvas is going to be defined by useditorDataContainer describes the size of the canvas, and block describes each component on the canvas

Part III Code

Complete code GitHub

Next section: left component menu, component drag rendering, and component selection and movement