How to mutate an array's value in “each” helper (Octane)

I have an array of strings passed as an argument to a component, inside the component I am using “each” helper to render each string in a text input. I tried the following approach.

MainComponent.hbs
<Component @model={{model}}/>

//Eg: model.list = ["Jack", "Sparrow"];

Component.hbs
<div>
    {{#each this.args.model.list as |name|}}
    <div>           
         <PaperInput @value={{ name }} 
            @placeholder="Enter a Name"
            @onChange={{action (mut name)}}/>        
    </div>
    {{/each}}
</div>

I am running into the error “Uncaught (in promise) Error: Assertion Failed: You can only pass a path to mut”. Would really appreciate if anyone can let me know What’s going wrong here.

You can only pass a property to mut, not a local variable. So this will not work that way.

You’ll need an action that looks like this:

@action
updateName(name, index) {
  this.args.model.list[index] = name;
}
 {{#each this.args.model.list as |name index|}}
    <div>           
         <PaperInput @value={{ name }} 
            @placeholder="Enter a Name"
            @onChange={{fn this.updateName index}}/>        
     </div>
{{/each}}

I haven’t tried this specifically, but it should work, I think.

1 Like

Thanks, this approach seems to be fine. But I am running into a problem with the statement this.args.model.list[index] = name; it does not change the array’s value and rerender the list. I am not finding the exact MutableArray method to accomplish this.

If it was an array cointaining objects the following would have worked

//mode.list   [ {name:"Jack"}, {name:"Sparrow} ]
@action
updateName(name, index) {
   set(this.args.model.list[index], "name", name);
}

But this does not work if the array just contains primitive types like strings.

The point is: the Array should know itself has been changed. For that ember extends the native array with MutableArray mixin to provide such methods to correctly update/create/remove/insert items in an array.

For your case, suppose you have a list of names like: ['Jack', 'Sparrow'],then you should use an index as:

@action
updateName(name, index) {
  this.args.model.list.replace(index, 1, [name]);
}

or if you have a collection of objects like: [ {name:"Jack"}, {name:"Sparrow} ], you can do this:

@action
updateName(name, index) {
  let target = this.args.model.list[index];
  target.name = name;
  this.args.model.list.replace(index, 1, [target]);
}

or even simpler:

@action
updateName(name, index) {
  this.args.model.list.replace(index, 1, [{ name }]);
}

or you can use a pattern that always updates the entire collection:

@action
updateName(name, index) {
  let list = this.args.model.list.map((item, idx) => {
    if (idx === index) return { name };
    else return item;
  });

  set(this.args.model.list, list);
}
1 Like

Thanks, I tried using ‘replace’ method. It indeed updates the array. But it makes the listr re render while I am still entering the text, causing the “input” element to lose the focus.

You might be able to avoid this by providing a key:

{{#each this.list key="@index"}}

This tells the each helper to re-render based on array index. I think that should work for you.

1 Like

This, my friend, is not a problem of ember.js, but a problem of your ui interactions.

Just think about it carefully:

  1. you use onChange as the hook to tell ember.js: “update the list whenever the input value has changed”
  2. my code (which using ember.js) do exactly what you say: update the list whenever the input value has changed
  3. so each time when a user types → the list gets updated → re-render → causing the input element loses its previous focus state

This is exactly what you tell ember.js to do, of course, it’s not what you desired to be.

So, what you really need is a confirmation mechanism, like an Update button. Then you should update the list when users click the button instead of in onChange hook.

1 Like

Thank you very much, that worked.

Template

{{#each this.args.model.list key="@index" as |name index|}}
    <div>           
         <PaperInput @value={{ name }} 
            @placeholder="Enter a Name"
            @onChange={{fn this.updateName index}}/>        
     </div>
{{/each}}

Component

 @action
 updateName( index, name ) {
    this.args.model.list.replace(index, 1, [name]);     
 }