Observe property on an array of objects for any changes

試著忘記壹切 提交于 2019-11-28 21:21:55

问题


I am using Aurelia and I have an array of items bound to a grid and they have a selected property on them. I want to bind a button to be enabled when any one of the items is true. I can do a brute force approach where I have a getter that is filtering the list and returning the selected items, but that means that I would be doing dirty checking constantly in the app and I don't want to do that. I am hoping for a more efficient approach. Any ideas?


回答1:


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




回答2:


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.




回答3:


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.




回答4:


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;
  }
}


来源:https://stackoverflow.com/questions/32017532/observe-property-on-an-array-of-objects-for-any-changes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!