Adding a delete row button from a Table

Hello so I kinda got thrust into the middle of EmberJS. I have worked a few tutorials but I am still struggling with something I feel like should be super simple. I am trying to add a delete row button for my table. Eveything is rendering correctly I just don’t think I am handling my Data correctly.

//import.hbs

            {{#each model as |file|}}
                <tr>
                    <td>{{file.name}}</td>
                    <td>{{file.size}} MB</td>
                    <td>{{file.type}}</td>
                    <td><button type="button" class="btn btn-outline-danger btn-sm" {{action 'removeFile'}}>Delete</button></td>
                </tr>
            {{/each}}

//routes/import.js

import {A} from '@ember/array';
import Route from '@ember/routing/route';

export default class ImportRoute extends Route {
    model() {
        return A();
    }
}

I am able to get the files that are being imported via a drag and drop box into my model and I can remove the files from the model as well but I guess my true issue is the Table does not update upon deleting a file.

//controllers/import.js

async removeFile(){
            let files = this.model;

            let fileNames = [];
            for(let i=0; i<files.length; i++){
                fileNames.push(files[i].name);
            }

            if(files[0].name == fileNames[0]){
                files.splice(0, 1) 
            }
        },

This is how I am removing said file and yes it is obviously only going to work for the first file rn I can work on that logic after but can anyone assist me please and thank you a ton in advance.

It looks like you need {{action "removeFile" file}} (the syntax says, pass file to the function removeFile) and haven’t used file that is passed to removeFile.

Conceptually, the function would look like,

removeFile(file) {
  // Get all files
  const files = ...;

  // Remove file by name (assumed to be unique)
  const remainingFiles = files.filter(({ name }) => file.name !== name);

  // Update list of files
  ...
}

I left ellipses here and there (left as exercise) because I don’t know how you intend to use A() returned from the model hook. :relaxed:

1 Like

Also worth noting this part is most likely due to using regular array methods like push that Ember has no visibility into. You caught Ember at an interesting time where the reactivity system we use is changing from pushed based to pull based. So what that means for you is that there are a number of ways to do what you want. The most straightforward at the moment is the “classic” solution, which is that Ember has a couple special methods for arrays so it can efficiently observe changes to them. So instead of <array>.push(<object>) you’d use <array>.pushObject(<object). This would notify the internal pushed based reactivity model that the array has changed and notify any observing consumers accordingly.

To use the new pull based reactivity system (you’ll want to be on Ember 3.15+ probably but it looks like you may be already) you’d use a tracked property. At the moment a tracked property with a complex object like an array requires resetting the entire array, like below, but there may be better built-ins in Ember core in the future:

class SomeComponent extends Component {
  @tracked items = [];

  @action
  pushItem(item) {
    let { items } = this;

    items.push(item);

    this.items = items;
  }
}

In this example anything that consumes this.items (including templates) will react automatically to the changes without having to observe it.

1 Like

The reason this works is because @tracked creates a getter/setter pair, and when you invoke the setter, it notifies the reactivity layer: “Hey, this thing updated, please re-render anything which depends on it!” With that in mind, I’d usually recommend one of the two following approaches (which feel a bit less odd than the this.items = this.items approach):

  1. Use a “functional update” style, where you create a new from a copy of the old:

    class SomeComponent extends Component {
      @tracked items = [];
    
      @action
      pushItem(item) {
        this.items = [...this.items, item];
      }
    }
    

    (You could also do this.items = this.items.concat(item).)

    That sets the items property, by creating a new array which is a copy of the old one and assigning it to the property.

  2. Use the tracked-built-ins library, which is likely very similar to what will end up shipped by Ember itself at some point. (The author is on the Framework Core team, and we’re using these extensively at LinkedIn already.) In that case, you can just create a tracked array and then use native methods directly and everything will Just Work™:

    import { TrackedArray } from 'tracked-built-ins';
    
    class SomeComponent extends Component {
      items = new TrackedArray();
    
      @action
      pushItem(item) {
        this.items.push(item);
      }
    }
    

    In this case, the TrackedArray is a very thin Proxy-powered wrapper around native arrays which intercepts access to the Array and notifies the reactivity system. Note that I haven’t marked the items property itself as @tracked here because I assume we’ll just have one stable tracked array, but if you wanted to be able to create a brand new array as well, you could do @tracked items = new TrackedArray() and then you could do either mutations to that array or creating a new tracked array at will.

There are important performance tradeoffs between these two approaches. The functional update style has to create a new array every time which copies the items from the old array. For very large arrays, this can get expensive, and using APIs which change the array in place (.push, .pop, .splice, etc.) can pay off. However, because TrackedArray uses a Proxy, it’s about 10× slower than a native array. This is still much faster than Ember’s EmberArray type, but it means that for small sets of data, the functional update can actually be faster than mutation with TrackedArray!

The absolute best performance will be the version @dknutsen showed, but it’s also the weirdest to read! For most app code, using the functional update style or TrackedArray will be more than fast enough, and I’d recommend you start there.

1 Like

Yeah this is a great deeper dive. Do we surface this or something like it anywhere in the guides? I know there’s the section on “Autotracking In-Depth”. I’m never quite sure how deep to go. Too much information can be overwhelming but not enough can be confusing.

So I guess that begs the question do you @chriskrycho and @ijlee2 have a recommendation for what to use as the go-to “introductory” version of a tracked array? I took that version from the original tracked RFC and it’s slightly awkward as you mention but it also seems fairly clear just from the code what/why it’s necessary. TrackedArray is great but obviously that currently requires an addon and it’s a new class instead of a POJA which can be a turn-off to some. So for my own reference what would be your first recommendation if someone is learning and just wants to get a tracked array going and worry about the rest later?

1 Like

I always start with the functional approach because it’s the easiest to explain with the least weirdness and has perfectly acceptable performance characteristics for ordinary app code. My recommendation internally for LinkedIn code migrating to (or being written fresh in!) Octane is to start there as a default and reach for the addon only if you’ve identified a concrete performance problem—and then to measure and make sure that it’s actually an improvement.

I would actually never recommend the combo of mutating methods followed by a set in general; I’d reach for that only in extremely performance-sensitive contexts, and I’d probably think about extracting a utility of some sort to do it with clear explanation of its weirdness and a warning not to reach for it “just because”. (There’s a real danger of ending up with tribal knowledge that “you should always do it this way because better performance” that becomes hard to dislodge.)

2 Likes

I like the functional/immutable approach more than writing this.items = this.items;. I recently got to use TrackedArray in an app; I liked the experience of using it as well as explaining to others why JavaScript’s .push() can work in this case. I didn’t realize there were performance tradeoffs among the 3 approaches so I’m glad to have come across by this thread.

I’ve been meaning to update the Ember Guides to show the functional approach for updating arrays. You can track the progress at https://github.com/ember-learn/guides-source/issues/1539.

1 Like

Also wanted to thank yall for taking the time to post very detailed and interesting replies on my issue. This has been very helpful!

@ijlee2 @chriskrycho @dknutsen

3 Likes

I appreciate your work guys and the answers in this topic, for me it is a new but useful information and I can safely put it into practice in the future. Thanks!