Issue using ref="" in a repeat.for with a value converter


#1

Hi All,
I have an array of data items, that I am displaying in HTML through a repeat.for, I am using a value converter to filter this data based on a search value. Each data item has a html element that is linked to the template via the use of a ref="". Everything appears to work correctly, however if I splice an item from my array, the refs appear to go out of sync, and point at the incorrect input. This behavior does not occur if I remove the value converter from the HTML. Hoping someone may be able to show me where I am going wrong here. I have provided a simple example below

My Data Item:

    export interface DataItem {
          displayName: string;
          id: string;
          rInputElement?: HTMLInputElement;
        }

View Model:

export class App
{
      dataItems: DataItem[];
      bTagSearchValue = '';
  
  public attached()
  {
    return this.getData();
  }

  getData()
  {
    // reset data
    this.dataItems = [];
    // create new data
    for (let index = 0; index < 6; index++)
    {
      let dataItem: DataItem = { displayName: "data Item" + index, id: index.toString() }

      this.dataItems.push(dataItem);
    }
  }

  deleteDataItem(id: string)
  {
    // grab the index of out item
    let itemIndex = this.dataItems.findIndex(item => item.id === id);

    // remove the item from our array
    this.dataItems.splice(itemIndex, 1);
  }

  updateDataItemName(id: string)
  {
    let itemIndex = this.dataItems.findIndex(item => item.id === id);

    let dataItem = this.dataItems[itemIndex];
    let itemDisplayName = dataItem.displayName;
    let itemRef = dataItem.rInputElement;
   
    let itemRefInputValue = dataItem.rInputElement.value;
    // itemDisplayName will not equal itemRefInputValue once an item has been spliced from dataItems
  }
}

My View :

  <template>

  <input value.bind="bTagSearchValue">

  <div repeat.for="item of dataItems | filter: ['displayName']:bTagSearchValue">
    <input value.bind="item.displayName" ref="item.rInputElement">
    <button click.delegate="updateDataItemName(item.id)">Rename Item</button>
    <button click.delegate="deleteDataItem(item.id)">Remove Item</button>
  </div>

</template>

The filter value converter I am using:

export class FilterValueConverter
{
    toView( array, property, query )
    {
        if ( query === void 0 || query === null || query === "" || !Array.isArray( array ) )
        {
            return array;
        }

    let properties = ( Array.isArray( property ) ? property : [property] ),
        term = String( query ).toLowerCase();

    return array.filter( ( entry ) =>
        properties.some( ( prop ) =>
            String( entry[prop] ).toLowerCase().indexOf( term ) >= 0 ) );
}

}

Any help would be greatly appreciated


#2

There is no immediately obvious solution for this, so what I would suggest you to do to solve your requirements is to have a simple AuArrayRef custom attribute:

@inject(Element)
@customAttribute('au-array-ref')
export class AuArrayRef {
  value: Element[];

  // the element annotated with this attribute
  private element: Element;
  constructor(element) {
    this.element = element;
  }

  unbind() {
    this.value = null;
  }

  valueChanged(newValue, oldArray) {
    if (Array.isArray(newValue) && !newValue.includes(this.element)) {
      newValue.push(this.element);
    }
    if (Array.isArray(oldArray) && oldArray.includes(this.element)) {
      oldArray.splice(oldArray.indexOf(this.element, 1));
    }
  }
}

You can have a look at online demo here https://codesandbox.io/s/xp7ymk4ywq (open the console and look at window.inputElements on sandbox frame)


#3

Hi bigopon, thanks for your response I will try what you have suggested. If possible would you be able to explain why my initial approach was not working? I’d be curious t understand where I was going wrong, or why such a combination wouldn’t work?

Thanks again! :slight_smile:


#4

I think I misunderstood you completely. But I’m still unable to see how things went wrong for you. Can you help explain via comment/ bullet points based on this codesandbox https://codesandbox.io/s/p7v745p70


#5

Hi bigopon, I added some comments here, hopefully this will help explain the issue a little clearer https://codesandbox.io/s/9yp85jv75y Its entirely possible that this should be working, and that I am missing something obvious, but at the moment I am stumped.
Thank you for your assistance with this :slight_smile:


#6

Thanks for asking about this, you just discovered a bug in Aurelia related to integration of ref binding and repeat. Ref binding is similar to oneTime binding, except it does not provide any way for Repeat to know that it should update the binding, in case of a mutation.

I deally the patch would look like the following:

import { NameBinder } from 'aurelia-binding';

NameBinder.prototype.updateOneTimeBindings = function() {
  if (this.isBound) {
    this.sourceExpression.assign(this.source, this.target, this.lookupFunctions);
  }
};

So repeat will be able to update the refbindings. Patched demo at https://codesandbox.io/s/kvql0xzz7 (I manually edit it to export NameBinder)

Unfortunately, NameBinder is not exported in Aurelia binding, so please wait for a PR :smiley:


A temporary work around is to not use value converter at all, but instead use a function to get the final array:

<div repeat.for="item of getDataItems(dataItems, ['displayName'], bTagSearchValue)">

It will use different kind of mutation handler and thus avoid the issue of ref binding not updating.


#7

Thank you so much for your help bigopon , really appreciate it! :slight_smile: I’ll go with that workaround for now :slight_smile: