computedFrom properties of objects in list


#1

Hey there, I’m a bit stuck in my brain to find a proper solution. In my application there is a model which got a list of child models. A getter isSelected returns a boolean if all children are selected. Dirty checking is not an option. How could I tell the binding engine to connect to the properties of all child objects?

class Parent {
  children = [];

  @computedFrom('children') // Will not trigger when only the property changed
  get isSelected() {
    if (!this.children.length) {
      return false;
    }
    return this.children.every(child => child.selected);
  }
}

My idea was to create a own computedFrom decorator which connects to changes of the items in the list and then connects to the children but so far it is tricky to understand the whole chain from parsing the binding interpolation to the observer on the getter.

Any idea from you guys?

Thanks in advance

Best
FragSalat


#2

What I typically do is keep track of a counter that gets updated each time a modification takes place. Something like:

class  Parent {
     children = [];
     counter = 0;


     toggleChildSelected(child) {
          child.selected = !child.selected;
          this.counter++;
     }

     @computedFrom('children', 'counter')
     get isSelected() { .... }
}

Hope this helps!


#3

If I have to watch a lot of properties on each list element I use the BindingEngine and dispose of all subscriptions in detached() and listChanged() and rebind them in attached() and listChanged() after the dispose.

For the update method I’ll just use a debounced function to wait until all list items have settled and 200ms later update anything dependent on selected.

import { BindingEngine } from 'aurelia-framework'; // Inject into view model

... 
// Rebind Subs
this.subscriptions = [];
this.list.forEach(item => {

   let sub = this.bindingEngine
      .propertyObserver(item, 'selected')
      .subscribe(() => this.delayedListUpdate());

   this.subscriptions.push(sub);

});

...
// Dispose Subs
this.subscriptions.forEach(sub => sub.dispose());
this.subscriptions = [];

I wish there was a more intuitive way to do this, but I haven’t seen it yet.


#4

Hey there, thanks for your suggestions but I don’t wanted to accept those not nice solutions and created my own decorator which allows me to tell that a getter is computed from list of objects.

I wrote a short blog post how it works and can be used https://blog.stagetwo.eu/entry/12-aurelia-computedfrom-array-of-objects/

The result looks something like this

class Child {
  selected: boolean = false;
}

class Parent {
  children: Child = [];

  @computedFromList('children[*].selected') // Binds selected property of each object to this getter
  get isSelected() {
    return this.children.every(child => child.selected);
  }
}

But I just figured out some limitations of Aurelia which requires the list of children not to be more than 100 for now.


#5

@fragsalat that’s creative :heart: and glad to see you solved it. But I think we should emphasize @Sjaak 's solution, as it’s often the most efficient way to handle any case.


#6

Definitely it would be the easier and probably better way to go but unfortunately our architecture doesn’t fit to that. The user can select 3 hierarchy levels like Grandparent, Parent, Child. Selecting Grandparents selects all Children of all Parents of those Grandparents. On the other wside when all children of a parent or all parents of a grandparent are selected, the grandparent is considered as selected as well. What I want to say with it is that the selection is not called on the parent or grandparent but always on children for having a generic selection logic which makes this construct not work. The children also doesn’t and shouldn’t have a relation to it’s parent.

Would be open for other architectural suggestions :slight_smile:


#7

I was only saying that to encourage folks to try @Sjaak solution before using yours. It seems to be suitable for most cases. But there are situations where yours makes more sense. I didn’t mean yours is not good.