templating-resources icon indicating copy to clipboard operation
templating-resources copied to clipboard

Expose all contained Bindings via View interface

Open HubertoKusters opened this issue 5 years ago • 4 comments

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

HubertoKusters avatar Apr 16 '19 06:04 HubertoKusters

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 avatar Apr 16 '19 11:04 bigopon

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

bigopon avatar Apr 16 '19 11:04 bigopon

@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?

fkleuver avatar Apr 21 '19 22:04 fkleuver

Hi,

This is great feedback and you are right. I am new to Aurelia and as such I am not aware of all the features Aurelia brings to the table. I had heard of Aurelia store but dismissed it because it seemed like overkill at first. But, I will take a better look at it. What I have not seen yet is the extensionpoint processAttribute. This is certainly worth a look as well. Also, the previous answer to my question by Bigopon suggesting traversing the dom for input-elements and retrieve the corresponding bindingins that way is something I will take a look at.

Thanks you guys for the input. I appreciate it.

On Mon, Apr 22, 2019 at 12:52 AM Fred Kleuver [email protected] wrote:

@HubertoKusters https://github.com/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?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/aurelia/templating-resources/issues/373#issuecomment-485287935, or mute the thread https://github.com/notifications/unsubscribe-auth/ABRMEDMKPES3ZHMXMDAVYKTPRTV2DANCNFSM4HGFYHRA .

HubertoKusters avatar Apr 21 '19 23:04 HubertoKusters