有些插件实现拖拽布局不是很流畅,这里直接简单实现通过左侧菜单组件,拖入到画板上实现样式,右侧可根据数据交互实现联动,也是参考了部分网上教程,后期根据自己思路修改后的结果,细看代码后,其实思路还是那一套,不停的套娃套出来的。
简单实现效果图就是 如下
![](https://img.haomeiwen.com/i14756798/dd4f0227594978cf.png)
1.左侧组件区域
//html
<div class="tool-list">
<ul>
<li *ngFor="let item of componentList" class="list-box" #dragPlaceholderCanvas appDrag
[componentType]="item.type" [dragPlaceholder]="dragPlaceholderCanvas">
<div class="label"> {{item.label}}</div>
<button color="primary">
拖拽
</button>
</li>
</ul>
</div>
//css
.tool-list {
border: 1px solid #333333;
width: 200px;
box-sizing: border-box;
padding: 10px;
border-radius: 4px;
overflow: hidden;
flex-basis: 200px;
height: 100%;
ul {
margin: 0;
padding: 0;
list-style: none;
li {
padding: 10px 0;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.label {
font-size: 18px;
}
}
.list-box {
border: 1px solid #ccc;
}
}
}
// ts
import { Component ,OnInit} from '@angular/core';
@Component({
selector: 'app-tool-list',
templateUrl: './tool-list.component.html',
styleUrls: ['./tool-list.component.scss']
})
export class ToolListComponent implements OnInit {
componentList = [
{label: '文本', type: 'text'},
{label: '列表', type: 'list'},
{label: '输入框', type: 'input'},
{label: '布局', type: 'layout'},
];
constructor() {
}
ngOnInit(): void {
}
}
2.右侧联动区域,这里只做了简单的交互模式,其实思路还是那样,数据绑定的逻辑,处理的是同一份json数据
//html
<div class="setting-box">
<div class="from-box" *ngIf="drawOrActiveItem">
<nz-divider nzPlain [nzText]="'组件名'+drawOrActiveItem.componentName"></nz-divider>
<div nz-row>
<div nz-col nzSpan="6">表单栅格</div>
<div nz-col nzSpan="18">
<nz-slider [(ngModel)]="drawOrActiveItem.styles.grid" [nzMax]="24"[nzMin]="1" ></nz-slider>
</div>
</div>
<div nz-row *ngIf="drawOrActiveItem.type==='layout'">
<div nz-col nzSpan="6">布局模式</div>
<div nz-col nzSpan="18">
<nz-radio-group [(ngModel)]="drawOrActiveItem.styles.type" nzButtonStyle="solid">
<label nz-radio-button nzValue="default">default</label>
<label nz-radio-button nzValue="flex">flex</label>
</nz-radio-group>
</div>
</div>
<div nz-row *ngIf="drawOrActiveItem.styles.type==='flex'">
<div nz-col nzSpan="6">水平排列</div>
<div nz-col nzSpan="18">
<nz-select [(ngModel)]="drawOrActiveItem.styles.justify" style="width: 100%;">
<nz-option nzValue="start" nzLabel="左侧"></nz-option>
<nz-option nzValue="end" nzLabel="右侧"></nz-option>
<nz-option nzValue="center" nzLabel="居中"></nz-option>
<nz-option nzValue="space-around" nzLabel="平均"></nz-option>
<nz-option nzValue="space-between" nzLabel="两端"></nz-option>
</nz-select>
</div>
</div>
<div nz-row *ngIf="drawOrActiveItem.type==='input'">
<div nz-col nzSpan="6">lable宽度</div>
<div nz-col nzSpan="18">
<nz-input-number [(ngModel)]="drawOrActiveItem.styles.layout.labelWidth" [nzMin]="1" [nzStep]="1" style="width: 100%;"></nz-input-number>
</div>
</div>
</div>
</div>
//css
.setting-box{
border: 1px solid #333333;
width: 350px;
box-sizing: border-box;
padding: 10px;
border-radius: 4px;
overflow: hidden;
flex-basis: 200px;
height: 100%;
.from-box{
width: 100%;
height: 100%;
}
}
//ts
import { AfterViewInit, Component, OnInit, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import { OnChangeType } from 'ng-zorro-antd/core/types';
@Component({
selector: 'app-setting-module',
templateUrl: './setting-module.component.html',
styleUrls: ['./setting-module.component.scss']
})
export class SettingModuleComponent implements OnInit {
@Input() drawOrActiveItem: any;
constructor() {
}
ngOnInit(): void {
}
ngOnChanges(): void {
console.log(this.drawOrActiveItem);
}
}
3.画板区域,也是核心交互的部分,这里一开始是通过很复杂的套娃实现后,发现代码可以简化,从200行的html套娃,简化成了100多行,以及在拖拽过程中,拖和放下两个关键动作的数据处理。
//html
<div class="container">
<div class="drawing-board" appDrop (dropEvent)="getDropEventData($event)" [dropData]="defaultComponentInfo">
<ng-container
*ngTemplateOutlet="colTpl; context: {rows: defaultComponentInfo.component,rowId:defaultComponentInfo.id}">
</ng-container>
</div>
<ng-container *ngIf="defaultComponentInfo.component.length===0">
<div class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</ng-container>
</div>
<!-- 套娃流式布局base -->
<ng-template #childTemplate let-rowItem="rows">
<div class="draw-col" [ngStyle]="{'width': 'calc(100% / ('+ (24/rowItem.styles.grid)+'))'}">
<div nz-row class="drawing-row-item" [ngClass]="{'active-drawing-row-item ':activeId===rowItem.id}"
(click)="handleActive($event,rowItem)" #dragPlaceholderCanvas appDrag [componentType]="rowItem.type"
[componentId]="rowItem.id" [dragPlaceholder]="dragPlaceholderCanvas"
(drawEndEvent)="getDrawEndEventData($event)" (drawStartEvent)="getDrawStartEventData($event)">
<span class="component-name">{{rowItem.componentName}}</span>
<ng-container *ngTemplateOutlet="dropArea; context: {rows: rowItem}"></ng-container>
<!-- 删除按钮组件 -->
<ng-container *ngTemplateOutlet="itemBtn; context: {rows: rowItem}"></ng-container>
</div>
</div>
</ng-template>
<!-- 拖拽内容 -->
<ng-template #dropArea let-rows="rows">
<div class="drag-wrapper" appDrop (dropEvent)="getDropEventData($event)" [dropData]="rows">
<ng-container *ngIf="rows.styles.type==='flex'">
<ng-container *ngIf=" rows.children && rows.children.length>0">
<div nz-row [nzJustify]="rows.styles.justify" class="drag-wrapper-row">
<ng-container *ngTemplateOutlet="colTpl;context: {rows:rows.children,rowId: rows.id}">
</ng-container>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="rows.styles.type==='default'">
<ng-container *ngIf=" rows.children && rows.children.length>0">
<ng-container *ngTemplateOutlet="colTpl;context: {rows:rows.children,rowId: rows.id}">
</ng-container>
</ng-container>
</ng-container>
</div>
</ng-template>
<!-- 列模块 -->
<ng-template #colTpl let-rows="rows" let-rowId="rowId">
<ng-container *ngFor="let row of rows">
<!-- 布局类型 -->
<ng-container *ngIf="row.type==='layout' ">
<ng-container *ngTemplateOutlet="childTemplate; context: {rows: row}"></ng-container>
</ng-container>
<!-- input类型 -->
<ng-container *ngIf="row.type==='input' ">
<div class="draw-col drawing-input-item" [ngClass]="{'active-drawing-row-item ':activeId===row.id}"
[ngStyle]="{'width': 'calc(100% / ('+ (24/row.styles.layout.grid)+'))'}" #dragPlaceholderCanvas appDrag
[componentType]="row.type" [componentId]="row.id" [dragPlaceholder]="dragPlaceholderCanvas"
(drawEndEvent)="getDrawEndEventData($event)" (drawStartEvent)="getDrawStartEventData($event)"
(click)="handleActive($event,row)">
<div class="input-item">
<div class="input-item-lable" [ngStyle]="{'width':row.styles.layout.labelWidth+'px'}">{{row.label}}
</div>
<div class="input-item-input" >
<input nz-input [(ngModel)]="row.dataBinding.fieldName" />
</div>
</div>
<!-- 删除按钮组件 -->
<ng-container *ngTemplateOutlet="itemBtn; context: {rows: row}"></ng-container>
</div>
</ng-container>
</ng-container>
</ng-template>
<!-- 删除组件 -->
<ng-template #itemBtn let-rows="rows">
<span class="drawing-item-delete" title="删除" (click)="handleDelete($event,rows)">
<span nz-icon nzType="delete" nzTheme="outline"></span>
</span>
</ng-template>
//css
.container {
width: 100%;
height: 100vh;
position: relative;
.drawing-board {
height: 100%;
position: relative;
.active-drawing-row-item {
border: 1px dashed #409EFF;
// 只有点击选中了 才会有删除按钮,必须加&,必须是邻父子关系,否则嵌套里面都会显示
&>.drawing-item-delete {
display: initial;
}
&>.input-item {
background: #f6f7ff;
border-radius: 6px;
}
}
z-index: 1;
}
.draw-col {
// display: inline-block;
float: left;
padding-left: 5px;
padding-right: 5px;
}
.drawing-row-item {
position: relative;
cursor: move;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border: 1px dashed #ccc;
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
&::after,
&::before {
display: table;
}
.draw-col {
margin-top: 20px;
}
// 套娃中的样式当前样式
.drawing-row-item {
margin-bottom: 2px;
}
// 文字样式
.component-name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: #bbb;
display: inline-block;
padding: 0 6px;
}
// 可draw位置的样式
.drag-wrapper {
min-height: 80px;
width: 100%;
}
// btn按钮样式
.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 20px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
right: 24px;
border-color: #F56C6C;
color: #F56C6C;
background: #fff;
}
}
.drawing-input-item {
position: relative;
cursor: move;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border: 1px dashed #ccc;
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
&::after,
&::before {
display: table;
}
.input-item {
width: 100%;
border-radius: 6px;
padding: 12px 10px;
margin-bottom: 2px;
display: flex;
align-items: center;
// 文本框设计层暂时不能编辑
pointer-events: none;
&::after,
&::before {
display: table;
content: "";
}
}
// btn按钮样式
.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 20px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
right: 24px;
border-color: #F56C6C;
color: #F56C6C;
background: #fff;
}
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: #ccb1ea;
letter-spacing: 4px;
z-index: 0;
}
}
//ts
import { AfterViewInit, Component, OnInit, Output, EventEmitter } from '@angular/core';
import { getIdGlobal } from '../../../../utils/db';
import { layoutUiComponents, inputUiComponents } from "../../../../utils/config";
@Component({
selector: 'app-draw-area',
templateUrl: './draw-area.component.html',
styleUrls: ['./draw-area.component.scss']
})
export class DrawAreaComponent implements OnInit, AfterViewInit {
@Output() checkedEvent: EventEmitter<any> = new EventEmitter();
defaultComponentInfo: any = {
//随机生成一个id
id: getIdGlobal(),
component: [],
style: {},
};
// 拽入的item
dropItem: any;
// 托的item或者点击选中的item
drawOrActiveItem: any;
// 记录一个id参数
idGlobal = getIdGlobal();
// 当前点击选中的id
activeId: any = getIdGlobal();
// 拖拽的模块的id
drawId: any;
constructor() {
}
ngOnInit(): void {
}
ngAfterViewInit() {
}
// 拽放下
getDropEventData(data: any): any {
// console.log(data);
// 如果从左侧组件拖入到画板中
if (!this.drawId) {
let config: any;
if (data.layout === 'layout') {
// 每次赋值都是最新的组件样式
config = JSON.parse(JSON.stringify(layoutUiComponents));
} else if (data.layout === 'input') {
// 每次赋值都是最新的组件样式
config = JSON.parse(JSON.stringify(inputUiComponents));
} else {
console.log('待研发');
return;
}
this.idGlobal = ++this.idGlobal;
if (config) {
config.id = this.idGlobal;
config.renderKey = `${config.id}${+new Date()}`;// 改变renderKey后可以实现强制更新组件
config.componentName = `row${this.idGlobal}`;
this.activeId = config.id;
if (!Array.isArray(config.children)) {
config.children = [];
}
// console.log(config);
if (data.currentAreaInfo.component) {
data.currentAreaInfo.component.push(config);
} else {
data.currentAreaInfo.children.push(config);
}
//TODO 这里最后停下的数据就是可以操作的数据
this.checkedEvent.emit(config)
}
} else {
this.dropItem = data;
}
// console.log(this.defaultComponentInfo);
}
// 从画布中-拖开始
getDrawStartEventData(data: any): void {
this.drawId = data.id;
}
// 从画布中-拖结束
getDrawEndEventData(data: any): void {
const activeItem = this.filterData(this.drawId, this.defaultComponentInfo.component);
this.drawOrActiveItem = activeItem;
// console.log(this.drawOrActiveItem)
// console.log(this.dropItem);
// 这里需要考虑,如果拖的地方没有拽接收
if (this.dropItem) {
if (!this.dropItem.currentAreaInfo.component) {
// 如果拖的块和放的块都是同一个,那么就放那里0101B350.png,不做交互
if (activeItem.id !== this.dropItem.currentAreaInfo.id) {
// 去掉被拖的
this.loopfilter(this.defaultComponentInfo.component, data.id);
// push拖的到新的位置
this.setDropData();
}
} else {
// 如果从里面托到最外层
// 去掉被拖的
this.loopfilter(this.defaultComponentInfo.component, data.id);
// push拖的到新的位置
this.setDropData();
}
} else {
this.drawId = null;
this.dropItem = null;
}
}
// 拽结束
setDropData(): void {
if (this.drawOrActiveItem) {
this.activeId = this.drawOrActiveItem.id;
if (this.dropItem.currentAreaInfo.component) {
this.dropItem.currentAreaInfo.component.push(this.drawOrActiveItem);
} else {
this.dropItem.currentAreaInfo.children.push(this.drawOrActiveItem);
}
//TODO 这里最后停下的数据就是可以操作的数据
this.checkedEvent.emit(this.drawOrActiveItem)
}
// 完了就情况拖拽的id
this.drawId = null;
this.dropItem = null;
// console.log(this.defaultComponentInfo);
}
// 递归删除数据
loopfilter = (arr: any[], valueId: any) => {
arr.forEach((item, index) => {
if (item.id === valueId) {
arr.splice(index, 1)
}
if (item.children.length > 0) {
return this.loopfilter(item.children, valueId)
}
})
}
// 递归查找数据
filterData = (drawId: any, arr: any) => {
for (const item of arr) {
if (item.id == drawId) return item;
if (item.children && item.children.length) {
const item2: any = this.filterData(drawId, item.children);
if (item2) return item2;
}
}
}
// 点击事件
handleActive(event: any, row: any): void {
event.stopPropagation();
// console.log(row);
this.activeId = row.id;
const activeItem = this.filterData(row.id, this.defaultComponentInfo.component);
this.drawOrActiveItem = activeItem;
//TODO这里点击的的数据就是可以操作的数据
this.checkedEvent.emit(this.drawOrActiveItem)
}
// 删除事件
handleDelete(event: any, row: any): void {
event.stopPropagation();
// console.log(row);
this.loopfilter(this.defaultComponentInfo.component, row.id);
console.log(this.defaultComponentInfo);
}
}
4.然后封装了两个简单的拖拽指令,通过拖的指令,到放在哪一个块上的指令。
//draw.ts
import { Directive, ElementRef, HostListener, Input, Output, EventEmitter } from '@angular/core';
import { saveDrawingItem } from '../../utils/db';
@Directive({
selector: '[appDrag]'
})
export class DragDirective {
// 拖拽的类型
@Input() componentType: string = '';
// 拖拽的id
@Input() componentId?: string;
// 拖动占位符
@Input() dragPlaceholder: HTMLElement | undefined;
el: ElementRef;
// 拖拽结束返回参数
@Output() drawEndEvent: EventEmitter<any> = new EventEmitter();
// 拖拽开始
@Output() drawStartEvent: EventEmitter<any> = new EventEmitter();
canvasConfig = {
width: 100,
height: 100,
};
constructor(el: ElementRef) {
this.el = el;
this.el.nativeElement.setAttribute('draggable', true);
this.el.nativeElement.style.cursor = 'move';
}
// 开始拖拽
@HostListener('dragstart', ['$event'])
dragstart(e: any) {
e.stopPropagation();
const dt = e.dataTransfer;
dt.effectAllowed = 'copy';
dt.setData('text/plain', this.componentType);
this.dragWidthCustomerImage(e);
if (this.componentId) {
this.drawStartEvent.emit({
id: this.componentId
})
}
}
// 拖拽完成
@HostListener('dragend', ['$event'])
dragend(e: any) {
e.stopPropagation();
if (this.componentId) {
this.drawEndEvent.emit({
id: this.componentId
})
}
this.resetCanvas(this.dragPlaceholder);
}
// 生成拖拽目标的阴影
dragWidthCustomerImage(event: any) {
event.dataTransfer.setDragImage(this.dragPlaceholder, 25, 25);
}
resetCanvas(canvasEl: any) {
}
}
//drop指令
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core';
import { DropDataInterface } from '../../pages/interfaces/drag.interface';
import { getDrawingItem } from '../../utils/db';
@Directive({
selector: '[appDrop]'
})
export class DropDirective {
el: ElementRef;
@Input()
dropData!: { id: string; layout: string; };
@Output() dropEvent: EventEmitter<DropDataInterface> = new EventEmitter();
@Output() dropEndEvent: EventEmitter<any> = new EventEmitter();
constructor(el: ElementRef) {
this.el = el;
}
@HostListener('dragover', ['$event'])
dragOver(e: any) {
e.preventDefault();
const target = e.target;
//因为dragover会发生在其他dom上,所以要判断是不是col
// if (e.target.matches(".draw-col ")) {
// console.log(target);
// const prev = target.previousElementSibling();
// const next = target.nextElementSibling();
// if (prev) {
// console.log("前一个",prev);
// }
// if(next){
// console.log("后一个",next);
// }
// }
return false;
}
// 拖拽完成
@HostListener('dragend', ['$event'])
dragend(e: any) {
console.log('拽完成')
e.stopPropagation();
// console.log(e);
}
// 拖拽离开当前选中的div
@HostListener('dragleave', ['$event'])
dragleave(e: any) {
e.preventDefault();
if (e.target.matches(".drawing-board ") || e.target.matches(".drag-wrapper") || e.target.matches(".drag-wrapper-row") ) {
console.log('拖拽离开选中div');
// console.log(document.getElementById('outBox'))
e.target.style.border = 'none';
}
}
// 拖拽移入后当前选中的div样式
@HostListener('dragenter', ['$event'])
dragenter(e: any) {
e.stopPropagation();
if (e.target.matches(".drawing-board ") || e.target.matches(".drag-wrapper") || e.target.matches(".drag-wrapper-row")) {
console.log('拖拽移入选中div')
// e.target.style.border = '1px solid red';
}
}
// 拖拽放下
@HostListener('drop', ['$event'])
drop(e: any) {
e.stopPropagation();
if (e.target.matches(".drawing-board ") || e.target.matches(".drag-wrapper") || e.target.matches(".drag-wrapper-row")) {
const text = e.dataTransfer.getData('text');
console.log('拖拽放下选中div')
this.dropEvent.emit({
layout: text,
currentAreaInfo: this.dropData,
});
}
}
}
5.最后附上组件的静态模式数据
export const layoutUiComponents = {
id: "",
type: "layout",
styles: {
grid: 11,
type: 'default',
justify: 'start',
align: 'top'
},
props: {
},
}
export const inputUiComponents = {
id: "",
label: "字段",
type: "input",
props: {
},
styles: {
layout: {
margin: "",
padding: "",
grid: 24,
labelWidth: 104,
showLabel: true,
}
},
dataBinding: {
fieldName: "field1",
fieldLable: "字段1",
fieldType: "string"
}
}
1.以上便是能够实现基础简单的拖拽功能,后期还可以拓展更多的组件交互,通过拖入不同的组件来实现拖拽布局,最后保存的json文件,即可作为低代码渲染的数据去渲染,仅仅是demo作为学习参考。
2.这里可以做拓展的方向也有如果实现拖拽排序,参考一个思路就是,把一个组件拖入到另一个组件的头上,然后松下鼠标这一个过程,获取到他放下的那个组件,然后处理他父级中children,进行删除,然后插入这个过程,其实就是数据处理把小标为1的替换到下标为2的位置上,核心就是知道拖的组件下标和放下的组件下标就可以了。
3.这里考虑到from输入框问题,所以得带上双向绑定的key和value,一定要把设计模式下的数据和最后渲染模式下数据理解区分开。
4.仅供参考学习,之前查找的参考资料链接没找到了,如果涉及到抄袭,可联系修改,谢谢。
网友评论