Hey folks
Today at work we made some changes in our Aurelia application which lead to some unforseen changes. The model setup is a parent class which holds a list of child classes. The parent class have a getter which returns true or false based on all children having a certain property or not.
This worked previously because the children array was itself modified and therefore the getter re-evaluated. Now the code changed so that only the specific property of the children change and the getter isn't called in this case. After wasting some time with Google and even ask the community in discourse I've found NO acceptable solution. So I checked how the computedFrom decorator works and wanted to create my own.
How it works
First of all I want to say that I might miss something here as I only had a shorter look. As far as I can see it tThe original computedFrom decorator just takes your string parameter list and puts it into the property dependencies on the getter function. Once the template is parsed the attributes and string interpolations are evaluated and bound (connected) to the related object. This connection is actually just a subscription to changes of those properties. The common logic uses the Aurelia Parser to parse the dependency expressions and connect on the outcome. And here we have the problem. The Aurelia Parser doesn't support wildcards but only clear results, which means only one specific target property. So what we need is a custom expression whose takes our dependencies and connects to all found target properties based on wildcards in the expression. To do so I created a custom expression which splits the dependency expression string on [*]. , interprets each part as own expression and connects to all results.
That said here is my result
- import {Parser, Expression, ComputedExpression, createOverrideContext} from 'aurelia-binding';
- class ComputedListExpression extends Expression {
- /**
- * @param {string} expressionStr Expression string for one dependency
- */
- constructor(expressionStr) {
- super();
- const parser = new Parser();
- this.expressions = expressionStr.split('[*].').map(part => parser.parse(part));
- }
- /**
- * Connect template interpolation to dependency. Once dependency is changed and subscribers are called this ensures
- * the binding is notified for changes.
- * @param {Binding} binding
- * @param {Scope} rootScope
- */
- connect(binding, rootScope) {
- let currentScopes = [rootScope];
- // Iterate trough expressions e.g. children => Array<Child>, isSelected => boolean
- for (let expression of this.expressions) {
- let scopes = [];
- currentScopes.forEach(scope => {
- scopes = scopes.concat(this.connectAndEvaluate(expression, binding, scope));
- });
- currentScopes = scopes;
- }
- }
- /**
- * Connect current expression to binding with current scope
- * @param {Expression} expression
- * @param {Binding} binding
- * @param {Scope} scope
- * @returns {Array<Scope>}
- */
- connectAndEvaluate(expression, binding, scope) {
- expression.connect(binding, scope);
- const scopes = [];
- let results = expression.evaluate(scope);
- // If result is not a list make it to one
- if (!Array.isArray(results)) {
- results = [results];
- }
- results.forEach(result => {
- scopes.push({bindingContext: result, overrideContext: createOverrideContext(result)});
- });
- return scopes;
- }
- }
- /**
- * Mark getter to be computed from a list. This decorator allows to use properties of objects in arrays as dependencies.
- * This works like common computedFrom except that arrays can be accessed by appending [*]. to properties of type array
- * @param {Array<string>} expressions Expressions like children[*].property
- * @returns {Function}
- */
- export function computedFromList(...expressions) {
- return function(target, property, descriptor) {
- const dependencies = expressions.map(expression => new ComputedListExpression(expression));
- descriptor.get.dependencies = new ComputedExpression(property, dependencies);
- };
- }
The usage is faily simple and similar to the original computedFrom decorator of Aurelia. If you have like me a property which holds an array of objects you can access each the properties of each object in that array by using a wildcard children[*].propname.
Here is an example
- import {computedFrom} from 'aurelia-binding';
- import {computedFromList} from './decorator';
- export class Child {
- selected = false;
- @computedFrom('selected')
- get isSelected() {
- return this.selected;
- }
- }
- export class Parent {
- children = [];
- // Will update once Child.selected updates :)
- @computedFromList('children[*].isSelected')
- get isSelected() {
- return this.children.every(child => child.isSelected)
- }
- }
Limitations
I just figured out some limitations by Aurelia which requires the list of children to be less than 100. The reason for that is that Aurelia holds a list of slotNames for the observers and after 99 there is none. After 99 it will result in using the slot name undefined. This leads to an infinite loop by searching for a free slot name as the counter can be infinite incremented but will not find something else than undefined. Possible solutions would be a Pull Request to Aurelia or create a reused observer instead of connecting to the expression. That might be something for further improvements but for now it's sufficient in my case
I hope that helps you to handle objects in an array in Aurelia like it helped me