Description
I'm submitting a feature request
- Library Version:
1.8.0
Please tell us about your environment:
-
Operating System:
Windows 7 -
Node Version:
10.8.0 -
NPM Version:
6.9.0 -
JSPM OR Webpack AND Version
webpack 4.29.6 -
Browser:
Chrome Version 73.0.3683.103 (Official Build) (64-bit) -
Language:
TypeScript 3.4.3
Current behavior:
View interface does not expose a method to get all contained bindings
Expected/desired behavior:
A method added to the View interface to access all contained bindings
-
What is the expected behavior?
A method added to the View interface to access all contained bindings -
What is the motivation / use case for changing the behavior?
I have created a custom attribute to monitor if binding values have changed. This is useful for activating certain form elements (like save-button) when changes have been made by the use.
Below you find the code:
import { bindingMode, customAttribute, inject } from "aurelia-framework";
import * as _ from "lodash";
@customAttribute("is-dirty", bindingMode.twoWay)
@inject(Element)
export class IsDirtyCustomAttribute {
private owningView: any = null;
private myView: any = null;
private bindings: any[] = null;
private readonly element: any = null;
constructor(element: any) {
this.element = element;
}
public created(owningView: any, myView: any): void {
this.owningView = owningView;
this.myView = myView;
}
public attached(): void {
// find all two-way bindings to elements within the element that has the 'dirty' attribute.
this.bindings = this.getBindings();
// intercept each binding's updateSource method.
let i = this.bindings.length;
const self = this;
while (i--) {
const binding = this.bindings[i];
binding.dirtyTrackedUpdateSource = binding.updateSource;
binding.dirtyTrackedOldValue = binding.sourceExpression.evaluate(binding.source, binding.lookupFunctions);
binding.dirtyTrackedFlag = false;
if (!!binding.updateTarget) {
binding.dirtyTrackedUpdateTarget = binding.updateTarget;
binding.updateTarget = function (newValue: any): void {
this.dirtyTrackedUpdateTarget(newValue);
this.dirtyTrackedOldValue = newValue;
this.dirtyTrackedFlag = false;
self.checkValue.apply(self);
};
}
binding.updateSource = function (newValue: any): void {
this.dirtyTrackedUpdateSource(newValue);
this.dirtyTrackedFlag = !self.equals(this.dirtyTrackedOldValue, newValue);
self.checkValue.apply(self);
};
}
}
public detached(): void {
// disconnect the dirty tracking from each binding's updateSource method.
let i = this.bindings.length;
while (i--) {
const binding = this.bindings[i];
binding.updateSource = binding.dirtyTrackedUpdateSource;
binding.dirtyTrackedUpdateSource = null;
if (!!binding.dirtyTrackedUpdateTarget) {
binding.updateTarget = binding.dirtyTrackedUpdateTarget;
binding.dirtyTrackedUpdateTarget = null;
}
}
}
private equals(a: any, b: any): boolean {
if (typeof (a) !== "object" || typeof (b) !== "object" || a == null || b == null) return a === b;
const aProps = _.filter(Object.getOwnPropertyNames(a), (i: string): boolean => i.indexOf("__") === -1);
const bProps = _.filter(Object.getOwnPropertyNames(b), (i: string): boolean => i.indexOf("__") === -1);
if (aProps.length === 0 || bProps.length === 0) return a === b;
const props = _.union(aProps, bProps);
return _.every(props, (p: string): boolean => {
return this.equals(a[p], b[p]);
});
}
private getBindings(): any[] {
const bindings: any[] = [];
const self = this;
if (this.myView) { console.log("yes"); }
_.forEach(this.owningView.bindings.filter(b => b.mode === bindingMode.twoWay && self.element.contains(b.target)),
binding => bindings.push(binding));
_.forEach(this.owningView.children, viewslot => {
if (viewslot.children) {
_.forEach(viewslot.children, view => {
if (view.bindings) {
_.forEach(view.bindings.filter(b => b.mode === bindingMode.twoWay && self.element.contains(b.target)),
binding => bindings.push(binding));
}
if (view.controllers) {
_.forEach(view.controllers, controller => {
if (controller.boundProperties) {
_.forEach(controller.boundProperties, boundProperty => {
if (boundProperty.binding && boundProperty.binding.mode === bindingMode.twoWay /* && self.element.contains(boundProperty.binding.target)*/) {
bindings.push(boundProperty.binding);
}
});
}
});
}
});
}
});
return bindings;
}
private checkValue(): void {
let dirtyFlag = false;
let j = this.bindings.length;
while (j--) {
if (this.bindings[j].dirtyTrackedFlag) {
dirtyFlag = true;
break;
}
}
if (this["value"] !== dirtyFlag) {
this["value"] = dirtyFlag;
}
}
}
This works great, however the main problem I have with this code is located in the getBindings function:
private getBindings(): any[] {
const bindings: any[] = [];
const self = this;
if (this.myView) { console.log("yes"); }
_.forEach(this.owningView.bindings.filter(b => b.mode === bindingMode.twoWay && self.element.contains(b.target)),
binding => bindings.push(binding));
_.forEach(this.owningView.children, viewslot => {
if (viewslot.children) {
_.forEach(viewslot.children, view => {
if (view.bindings) {
_.forEach(view.bindings.filter(b => b.mode === bindingMode.twoWay && self.element.contains(b.target)),
binding => bindings.push(binding));
}
if (view.controllers) {
_.forEach(view.controllers, controller => {
if (controller.boundProperties) {
_.forEach(controller.boundProperties, boundProperty => {
if (boundProperty.binding && boundProperty.binding.mode === bindingMode.twoWay /* && self.element.contains(boundProperty.binding.target)*/) {
bindings.push(boundProperty.binding);
}
});
}
});
}
});
}
});
return bindings;
}
This function has to dig too deep into the internals of Aurelia and as a result, this might fail some day. Furthermore, due to the fact I have to dig so deep I cannot make use of the View type, which I rather did.
So, my question is: could such information be exposed via an official interface method in View?
Any thoughts?
TIA