Observe property on an array of objects for any changes

≡放荡痞女 提交于 2019-11-30 00:26:08

Few things you could do- assuming I have your use case right:

dirty-checking (it's only one property- not a big deal)

export class Item {
  selected = false;
}

export class ViewModel {
  items = [new Item(), new Item(), new Item()];

  get anySelected() {
    var items = this.items, i = items.length;
    while(i--) {
      if (items[i].selected) {
        return true; 
      }
    }
    return false;
  }
}

observe the items

import {BindingEngine, inject} from 'aurelia-framework';

export class Item {
  selected = false;
}

@inject(BindingEngine)
export class ViewModel {
  items = [new Item(), new Item(), new Item()];    
  anySelected = false;
  subscriptions = [];

  constructor(locator) {
    this.bindingEngine = bindingEngine;
  }

  updateAnySelected() {
    var items = this.items, i = items.length;
    while(i--) {
      if (items[i].selected) {
        this.anySelected = true;
        return;
      }
    }
    this.anySelected = false;
  }

  activate() {
    var items = this.items, i = items.length, observer;
    while(i--) {
      observer = this.bindingEngine.propertyObserver(items[i], 'selected');
      subscriptions.push(observer.subscribe(() => this.updateAnySelected());
    }
    this.updateAnySelected();
  }

  deactivate() {
    let dispose;
    while(subscription = subscriptions.pop()) {
      subscription.dispose();
    }
  }
}

use a collection class

import {computedFrom} from 'aurelia-framework';

export class Item {
  _selected = false;

  constructor(parent) {
    this.parent = parent;
  }

  @computedFrom('_selected')
  get selected() {
    return this._selected;
  }
  set selected(newValue) {
    newValue = !!newValue;
    if (newValue === _selected) {
      return;
    }
    _selected = newValue;
    this.parent.itemChanged(newValue);
  }
}

export class Items {
  items = [];
  selectedCount = 0;
  anySelected = false;

  createItem() {
    let item = new Item(this);
    this.items.push(item);
    return item;
  }

  itemChanged(selected) {
    this.selectedCount += (selected ? 1 : -1);
    this.anySelected = this.selectCount > 0;    
  }
}

export class ViewModel {
  items = new Items();

  constructor() {
    let item = this.items.createItem();
    item = this.items.createItem();
    item = this.items.createItem();
  }
}

use a selectedItems array instead of a selected boolean prop

export class ViewModel {
  items = [{}, {}, {}];
  selectedItems = [];

  selectItem(item) {
    this.items.push(item);
  }

  deselectItem(item) {
    this.items.splice(this.items.indexOf(item), 1);
  }
}

for binding purposes, use selectedItems.length as your "any selected" property

In addition to the Jeremy's examples, you can create a custom setter, for example:

class Item {
   // this is your ~private~ field
  _isSelected = false;

  // in our constructor, we pass the view model and the property name
  constructor(vm, prop, name) {
    this.vm = vm;
    this.prop = prop;
    this.name = name;
  }

  get isSelected() {
    return this._isSelected;
  }
  // when you set the value, you increase the vm's property
  set isSelected(value) {
    if (value !== this._isSelected) {
      this.vm[this.prop] += value ? 1 : -1;
      this._isSelected = value;
    }
  }
}

export class MyViewModel
{
  items = [];
  itemsSelected = 0; // that's the property we'll pass to the class we've created

  constructor() 
  {
    for (let i = 0; i < 50; i++) {
      // instead of adding a annonymous {} here, we add an instance of our class
      this.items.push(new Item(this, 'itemsSelected', `Item ${i+1}`));
    }
  }

  toggleIsSelected(item) {
    item.isSelected = !item.isSelected;
  }
}

I've created a plunker for you: http://plnkr.co/edit/OTb2RDLZHf5Fy1bVdCB1?p=preview


Doing that, you'll never be looping to see if some item has changed.

I think you can also leverage EventAggregator as shown here. In that way there is no need to perform dirty checking all the time and instead handle the item selection event in its own VM and publish the eventdata; the subscriber on the other side will listen to the same and perform the gymnastic needed.

However, I have never used it, so I am not sure about the deeper details of it. But from the documentation it looks pretty easy.

Jeremy got me thinking about this in this bug. So it looks like you can also get the binding refreshing via a custom Binding Behaviors. Hopefully Jeremy can confirm I'm not doing anything too silly here.

Used like this:

repeat.for="item of items | filter & array:'propertyName'"

It overrides the standard observe behaviour and observes on the array and the property you define on each item. It can probably be improved to be more generic...

function observeProperty(obj, property) {
  this.standardObserveProperty(obj, property);
  let value = obj[property];
  if (Array.isArray(value)) {
    this.observeArray(value); 
    for(let each of value){   
      this.standardObserveProperty(each, this.propertyName); 
    }
  }
}

export class ArrayBindingBehavior {
  bind(binding, source, property) {
    binding.propertyName = property;
    binding.standardObserveProperty = binding.observeProperty;
    binding.observeProperty = observeProperty;
  }

  unbind(binding, source) {
    binding.observeProperty = binding.standardObserveProperty;
    binding.standardObserveProperty = null;
    delete binding.propertyName;
  }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!