Search code examples
javascriptthree.jsfabricjs

Issue on re-render list when removing an item from an array javascript


I'm trying to create a lists for texts which user can delete and edit. but i got an issue when trying to implementing it, here's what my current development.

To create the list, i render elements by mapping the object using this function

function textContainer(e, color) {
    var textForm = `
        <form onsubmit="return false" class="form-inline my-2 text-form row" style="display:flex; justify-content:space-around;">
            <input class="col-9 form-control mr-sm-2" type="text" id="newText"
                placeholder="Name, Number etc">
            <button class="col-2 btn btn-outline-primary my-2 my-sm-0" type="button"
                onclick="addText(newText.value)">Add Text</button>
        </form>
    `;
    var textLayer = '';
    var id = 0;
    var iconColor = 'black';
    if (color != null) {
        iconColor = color
    }
    canvas._objects.map(object => {
        if (object.text != undefined) {
            console.log(object.fill)
            id += 1;
            textLayer = `
            <ul class="listTexts" style="padding :0;">
                <li id="text-`+ id + `" class="buttonLists">
                    <span>`+ id + `. ` + object.text + `</span>
                    <div>
                        <i class="fas fa-trash-alt" style="margin:0px 5px;" onclick="removeTextById(`+ id + `)"></i>
                    </div>
                </li>
            </ul>
            `;
        }
    });
    if (canvas._objects.length == 1) {
        $('.textContainer').append(textForm).html();
    } else if (e == 'removed') {
        $('.textContainer').append(textForm + textLayer).html();
    } else if (e == 'changed') {
        $('.listTexts').empty();
        $('.listTexts').append(textLayer).html();
    } else {
        $('.textContainer').append(textLayer).html();
    }
}

and removing the item based on their id like this

function removeTextById(id) {
    var array = canvas._objects.filter(function (el) {
        return el.text != null
    })
    canvas.remove(array[id - 1]);
    $('.textContainer').empty();
    textContainer('removed');
    canvas.renderAll();
}

this is works well, if it's only contains 1 or 2 item on the list, i can delete any item. But when it contains more than 2, for example 3, it'll remove 2 li tag, if it's 4 it'll remove 3 li tag. I dont know what happen, but my objects is correct, it only deleting 1 item. You can see it in action here https://natestudio.my.id/3d-configurator on text tab.

anyone have any idea why is that happening?

EDIT

canvas in my app is fabricjs canvas, and canvas._objects return lists of object inside that canvas. I'll try to create a simple reproduce in a minute

EDIT 2

Here's a minimal reproduce https://jsfiddle.net/2758bskq/6/


Solution

  • At the removal action, the list is displayed here:

    else if (e == 'removed') {
        $('.textContainer').append(textForm + textLayer).html();
    

    Before this executes, $('.textContainer') is empty. As textLayer contains one item only (see the loop above it), at most one item will be displayed after a removal.

    So one problem is the collection of textLayer: it should collect all relevant items, not just the last one. So replace:

    textLayer =
    

    with:

    textLayer +=
    

    But this will have bad consequences for other scenarios (addition, ...etc). To avoid that, just rebuild the list always.

    So replace if (canvas._objects.length == 1) { and the whole if..else structure that follows it with just:

    $('.textContainer').empty().append(textForm + textLayer);
    

    This will fix the problem you mentioned.

    Snippet (with canvas size reduced, so to focus on the list):

    var canvas = this.__canvas = new fabric.Canvas('canvas', {
      backgroundColor: 'white',
      centeredScaling: true,
    });
    
    function addText(text) {
      if (text != '') {
        jerseyName = new fabric.IText(text, {
          fontSize: 25,
          textAlign: 'center',
          fontWeight: 'bold',
          left: 100,
          top: 280,
          originX: 'center',
          originY: 'center',
          selectable: true
        });
        canvas.add(jerseyName);
        canvas.setActiveObject(jerseyName);
        textContainer();
        $('.text-form').each(function() {
          this.reset();
        });
      }
    }
    
    function textContainer() {
      var textForm = `
        <form onsubmit="return false" class="form-inline my-2 text-form row" style="display:flex; justify-content:space-around;">
          <input class="col-9 form-control mr-sm-2" type="text" id="newText"
            placeholder="Name, Number etc">
          <button class="col-2 btn btn-outline-primary my-2 my-sm-0" type="button"
            onclick="addText(newText.value)">Add Text</button>
        </form>
      `;
      var textLayer = '';
      var id = 0;
      canvas._objects.map(object => {
        if (object.text != undefined) {
          id += 1;
          textLayer += `
          <ul class="listTexts" style="padding :0;">
            <li id="text-` + id + `" class="buttonLists">
              <span>` + id + `. ` + object.text + `</span>
              <div>
                <button  onclick="removeTextById(` + id + `)">Remove</button>
              </div>
            </li>
          </ul>
          `;
        }
      });
      $('.textContainer').empty().append(textForm + textLayer);
    }
    
    function removeTextById(id) {
      var array = canvas._objects.filter(function(el) {
        return el.text != null
      })
      canvas.remove(array[id - 1]);
      textContainer();
      canvas.renderAll();
    }
    
    textContainer();
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <canvas id="canvas" height="10" width="400"></canvas>
    
    <div class="tab-pane p-3 textContainer" id="tabs-2" role="tabpanel" aria-labelledby="two-tab">
    </div>