0

Looks so simple, yet I don't know how to solve this efficiently.

I have two arrays activeClasses and doneClasses that each contain JavaScript Objects as their elements.

Each element should be able to be marked as "active" or "done" and should be deleted from the current, and added to the other array if its status changes after clicking "Save". How can I achieve this without mixing up my array indices? Behaviour is as expected except when selecting multiple elements:

https://stackblitz.com/edit/angular-etzocz?file=src%2Fapp%2Fapp.component.html

TS

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  activeChanged:Array<boolean> = [];
  doneChanged:Array<boolean> = [];

  toggleActive(i) {
    this.activeChanged[i] = !this.activeChanged[i];
    console.log('activeChanged:');
    console.log(this.activeChanged);
  }

  toggleDone(i) {
    this.doneChanged[i] = !this.doneChanged[i];
    console.log('doneChanged:');
    console.log(this.doneChanged);
  }

  save() {

    var activeToBeDeleted:Array<number> = [];
    var doneToBeDeleted:Array<number> = [];

    //Check if active classes have changed
    this.activeChanged.forEach(function (elem, index) {
      //Has changed
      if (elem) {
        this.doneClasses.push(this.activeClasses[index]);
        //Add to activeToBeDeleted
        activeToBeDeleted.push(index)
      }
    }.bind(this))
    //Check if done classes have changed
    this.doneChanged.forEach(function (elem, index) {
      //Has changed
      if (elem) {
        this.activeClasses.push(this.doneClasses[index]);
        //Add to doneToBeDeleted
        doneToBeDeleted.push(index)
      }
    }.bind(this))

    console.log('before deletion')
    console.log(this.activeClasses)
    console.log(this.doneClasses)

    //Delete array elements that were changed
    activeToBeDeleted.forEach(function(elem) {
      this.activeClasses.splice(elem,1)
    }.bind(this))

    doneToBeDeleted.forEach(function(elem) {
      this.doneClasses.splice(elem,1);
    }.bind(this))

    console.log('after deletion')
    console.log(this.activeClasses)
    console.log(this.doneClasses)

    //Rewrite activeChanged and doneChanged arrays again with false
    this.activeChanged = new Array(this.activeClasses.length).fill(false)
    this.doneChanged = new Array(this.doneClasses.length).fill(false)



  }


  //As from database
  activeClasses:Array<Object> = [
    {
     name: 'test1'
    },
    {
      name: 'test2'
    }
  ];


  doneClasses:Array<Object> = [
    {
    name: 'test3'
    },
    {
      name: 'test4'
    }
  ];

  ngOnInit() {
    //Fill activeChanged and doneChanged with false by default
    this.activeChanged = new Array(this.activeClasses.length).fill(false)
    this.doneChanged = new Array(this.doneClasses.length).fill(false)
  }

}

HTML

<div *ngFor="let active_class of activeClasses; let i = index" style="background-color: blue; text-align: center; padding: 20px; color: white;">
  <button *ngIf="!activeChanged[i]" (click)="toggleActive(i)">Mark as done</button>
  <button *ngIf="activeChanged[i]" (click)="toggleActive(i)">Mark as active</button>
  {{ active_class.name }}
</div>
<div *ngFor="let done_class of doneClasses; let i = index" style="background-color: red; text-align: center; padding: 20px; color: white;">
  <button *ngIf="!doneChanged[i]" (click)="toggleDone(i)">Mark as active</button>
  <button *ngIf="doneChanged[i]" (click)="toggleDone(i)">Mark as done</button>
  {{ done_class.name }}
</div>
<button (click)="save()">Save</button>
1

1 Answer 1

1

It's because when you splice the items in natural sort order, the array indexes change for the items after the first one you remove.

The solution is to do call reverse() before splicing, which allows you to progress down the array without impacting indexes.

This fixes it:

//Delete array elements that were changed
activeToBeDeleted.reverse().forEach(function(elem) {
   this.activeClasses.splice(elem,1)
}.bind(this))

doneToBeDeleted.reverse().forEach(function(elem) {
  this.doneClasses.splice(elem,1);
}.bind(this))

Why is it working?

First, activeChanged and doneChanged are arrays storing booleans at the index of the item modified (active, or done, see toggle methods). When you first loop over these arrays in the Save method, it loops over the items in ascending order, and thus, you are storing the indexes in ascending order into the activeToBeDeleted and doneToBeDeleted arrays.

So, after that, when you loop over the activeToBeDeleted and doneToBeDeleted arrays and delete from the activeClasses or doneClasses, then while the first delete works, none of the other deletes can work, because the first delete action removed an item from the beginning of the array, and caused all following indexes to be shifted and incorrect.

The solution works because by reversing the list of indexes (going in descending order), you are deleting from the end of the arrays working towards the beginning, which naturally preserves all the indexes. I'd recommend you use pen and pencil, it's a classic pattern actually.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you, that works. Could you explain why this is the case please?
Yup, I added details. It's a classic issue actually. When you have an array containing the indexes of items to remove from another array, and these indexes are in ascending order, then you must do the deletes from the end of the array going towards the beginning, hence the reverse
It'd be nice if you could accept the answer though, thanks.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.