How to prevent rearrange of children elements upon deleting one child in angular

126 views Asked by At

I have a parent angular component that displays children components with ngFor directive. Each child acts as an individual window within the parent and their position can be rearranged by CdkDrag. And I also created a small "X" button on the right top corner to close the child component. And when I hit "x" button to close one child with low index (e.g., 1 or 2 in the stackbliz example below), the other children are rearranged automatically. Is there a way to prevent such rearrangements and stay as is when closing any child window?

child component

@Input('target') target: string = '';
@Input('index') index: string = '';
@Output() onClose: EventEmitter<number> = new EventEmitter();

closeModal() {
  const i: number = +this.index;
  this.onClose.emit(i);
}

child template

<div class="example-box" cdkDrag>
  {{target}}
  <button class="CloseButton" (click)="closeModal()">X</button>
</div>

child css

.example-box {
  width: 100px;
  height: 100px;
  border: solid 1px #ccc;
  color: rgba(0, 0, 0, 0.87);
  display: flex;
  justify-content: center;
  position: relative;
  resize: both;
}

.CloseButton {
  position: absolute;
  top: 10px;
  right: 10px;
}

parent component

  names: string[] = ['1', '2', '3'];
  modalClosed(id: any) {
    this.names.splice(id, 1);
    console.log(id);
  }

parent template

<div class="ParentMain">
  <child-comp
    *ngFor="let name of names ; index as i"
    (onClose)="modalClosed($event)"
    target="{{name}}"
    index="{{i}}"
  >
  </child-comp>
</div>

parent css

.ParentMain {
  display: flex;
}

Complete stackbliz example

Stackbliz example code

4

There are 4 answers

2
Eliseo On BEST ANSWER

There're another approach, that I remember used in this SO

If we imagine a cdkDropList with "items" inside we can do some like

<div
  cdkDropList
  #doneList="cdkDropList"
  [cdkDropListData]="done"
  class="drag-zone"
  cdkDropListSortingDisabled="true"
>
  <div
    *ngFor="let item of done;let i=index"
    cdkDrag
    class="item-box"
    [style.top.px]="item.y"
    [style.left.px]="item.x"
    [style.z-index]="item['z-index']"
    (cdkDragStarted)="changeZIndex(item)"
    (cdkDragDropped)="changePosition($event, item)"
  >
    <child-comp
      class="item-box"
      [target]="item.name"
      [index]="i"
      (onClose)="modalClosed($event)"
    >
    </child-comp>
    <div *cdkDragPlaceholder class="field-placeholder"></div>
  </div>
</div>

Yes a cdkDropList na be different that a list!

  names: string[] = ['1', '2', '3'];
  done=this.names.map((x,index)=>({name:x,x:index*100,y:0,"z-index":0}))


  modalClosed(id: any) {
    this.done.splice(id, 1);
    console.log(id);
  }

  @ViewChild('doneList', { read: ElementRef, static: true }) dropZone: ElementRef;

  changeZIndex(item: any) {
    this.done.forEach((x) => (x['z-index'] = x == item ? 1 : 0));
  }
  changePosition(event: CdkDragDrop<any>, field:any) {
    const rectZone = this.dropZone.nativeElement.getBoundingClientRect();
    const rectElement =
      event.item.element.nativeElement.getBoundingClientRect();

    let y = +field.y + event.distance.y;
    let x = +field.x + event.distance.x;
      field.y = y;
      field.x = x;
      this.done = this.done.sort((a, b) =>
        a['z-index'] > b['z-index'] ? 1 : a['z-index'] < b['z-index'] ? -1 : 0
      );
  }

And some of .css

.drag-zone{
  position:relative;
  flex-grow:1;
  height: 20rem;
  border:1px solid silver;
  overflow: hidden;
}
.item-box {
  position:absolute;
}

//see that you die a shadow to de component
//using .cdk-drag-preview + selector of the child component
.cdk-drag-preview child-comp {
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
              0 8px 10px 1px rgba(0, 0, 0, 0.14),
              0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
drag-zone
.cdk-drag-placeholder {
  opacity: 0;
}

Stackblitz

1
John Van On

It's belonged to how you handle the items position when drag and drop, you need to save item position after onDragEnded event, you can see example here:

1
8ytan On

If you want to preserve the position of each dialogue after the close button is pressed, then there are a number of ways to do it. The simplest is to hide the dialogue when the button is clicked instead of removing them from the template.

Here's an example of doing just that: https://stackblitz.com/edit/angular-pqf4je-aatkjg?file=src%2Fapp%2Fchild-comp.html

In the above, clicking the dialogue causes the item to disappear but the remaining items shouldn't move because the item is in fact still there, it's just been set to opacity: 0.

A slightly more advanced approach that doesn't result in lots of invisible components on the screen would be to use CSS to position the items in specific positions - that way they won't move when one is removed.

0
Eliseo On

When cdk drag, change the style to the element adding a transform:translate3D. This is the reason that if change the "origin" (you have your elements in a div with flex) the elements arrange in another place.

You can avoid it if, using the event endDrag you give a position absolute and the position top and left. but before, instead makes the "cdkDrag" to a div in a chuild, you can drag the whole element

<!--See the template reference variable "wrapper"-->
<div #wrapper style="position:relative;display:flex" >
 
  <!--in cdkDragStarted I pass the component-->
  <child-comp #el cdkDrag (cdkDragStarted)="dragStart(el)" 
                          (cdkDragEnded)="dragEnd($event,i)" 
                          (onClose)="modalClosed($event)"
    *ngFor="let name of names ; index as i"
    target="{{name}}"
    index="{{i}}"
  >
  </child-comp>
</div>

In your child component we are going to do accesive the elementRef inject in constructor

//In child
constructor(public elementRef:ElementRef){}

Well, the dragEnd like

  dragEnd(event:CdkDragEnd)
  {
    //get the element:
    const el=event.source.element.nativeElement

    //change the style to position absolute
    el.style.position='absolute'
    el.style.transform=''
    el.style.top=(this.oldPosition.y+event.distance.y)+'px'
    el.style.left=(this.oldPosition.x+event.distance.x)+'px'
  }

As you're using a very old version of cdk-drag, you have no the "position" in the event, so we need work a bit in when we startDrag

  dragStart(component:any)
  {
    const posWrapper=this.container.nativeElement.getBoundingClientRect()
    const pos=component.elementRef.nativeElement.getBoundingClientRect()
    this.oldPosition={x:pos.x-posWrapper.x,y:pos.y-posWrapper.y}
    const el=component.elementRef.nativeElement;
    el.style.top=''
    el.style.left=''
    el.style.position='relative'

  }

The last is make the "wrapper" converve the width and heigth when all the elements are dragged

  ngAfterViewInit()
  {
    const posWrapper=this.container.nativeElement.getBoundingClientRect()
    this.container.nativeElement.style.width=posWrapper.width+'px'
    this.container.nativeElement.style.height=posWrapper.height+'px'
  }

A stackblitz