Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose all contained Bindings via View interface #373

Open
HubertoKusters opened this issue Apr 16, 2019 · 4 comments
Open

Expose all contained Bindings via View interface #373

HubertoKusters opened this issue Apr 16, 2019 · 4 comments
Labels

Comments

@HubertoKusters
Copy link
Contributor

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

@bigopon
Copy link
Member

bigopon commented Apr 16, 2019

I cannot make use of the View type, which I rather did.

Could you elaborate this? What View type and what would you rather do?

The code you are having should work great, and should not break in future. About an official method, I guess it's not too bad to have a method to recursively walk and gather all bindings into an array.

cc @EisenbergEffect and @fkleuver

@bigopon
Copy link
Member

bigopon commented Apr 16, 2019

Beside that, is this appropriate to say it's quite specific to form in general, and could be easily done via

  • injected element in custom attribute
  • using injected to query all input elements (input, select) and get the corresponding bindings

@fkleuver
Copy link
Member

@HubertoKusters

The functionality you're implementing in attached() and detached() is normally what you do in bind() and unbind() of a binding behavior. But you don't want to put the same binding behavior on every single binding in a view, that's just messy, so you grab all the bindings via a custom attribute and apply the functionality like that. It makes perfect sense.

Now, you're asking for an official API to get all bindings from a view, but isn't deep observation with dirty state tracking what you're really looking for?
I have solved a very similar problem in a few instances before, also for enabling/disabling save and undo buttons etc. I (and I'm sure others as well) would prefer an official API for actually solving that problem.

We do kind of already have this, it's aurelia-store. From an architectural point of view that's probably going to give you the cleanest and most robust way of accomplishing this.

I don't see any real issues with exposing an official API for getting all bindings but for me, the necessity of doing that in the first place is indicative of either some other higher level API missing in the framework, or the user not being aware of certain features already existing in other API's.

Apart from deep observation with state tracking, I honestly can't really imagine a use case for retrieving all bindings. There are also other extension points (e.g. processAttributes) that allow bulk-modifying bindings without the need to traverse like this.

So before we consider further increasing the API surface in vCurrent please let me know what you think about any of the above. Have you tried / considered trying store and/or @processAttributes? Do they miss anything?

@HubertoKusters
Copy link
Contributor Author

HubertoKusters commented Apr 21, 2019 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants