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?
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;
}
}
来源:https://stackoverflow.com/questions/32017532/observe-property-on-an-array-of-objects-for-any-changes