Skip to content

Expose all contained Bindings via View interface #373

Open
@HubertoKusters

Description

@HubertoKusters

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions