fast-text-field value binding in aurelia1 & aurelia2 issues

Created on 2 Aug 2020  路  19Comments  路  Source: microsoft/fast

Describe the bug; what happened?

Using the fast-text-field input with a value binding of two-way or to-view will not work as expected. If the bound value in the view model is set before bound to the text-field it will display blank rather than showing the value. For two-way this is extended where it will actually write a blank value back to the VM overriding what it is set to on bind.

What are the steps to reproduce the issue?
Test.html:
<fast-text-field value.two-way="prop" />
In the viewmodel assign a value to prop initially:
Test.ts:
class Test { @bindable prop : string = "bob"; }
You will see prop is overwritten with an empty string.

What behavior did you expect?
It should take the initial value from the VM if there is one, it certainly should not override it on bind (for two way).

It should behave the same as <input>

fast-element bug help-wanted request in-progress

All 19 comments

Thanks @mitchcapper We'll try to take a look at this soon to figure out what's going on there. Sounds like a potential timing issue with web components lifecycle vs. Aurelia's bind lifecycle.

Just to be sure, can you let me know what version of the packages you are using? There was a release late last week that fixed a number of form issues.

@mitchcapper Can you detail the difference between what you are seeing with Aurelia 1 vs. Aurelia 2. I'm not quite following there.

Also, thanks for the stackblitz link. That will help get to the bottom of this.

@mitchcapper FYI I'm talking to the Aurelia team as well. One related issue is that when using 2-way binding on a custom element with Aurelia, you have to configure Aurelia so that it knows how to bind in both directions. The configuration is different for au1 vs. au2. The Aurelia team will follow-up here with more information. We can then take that info and also update the FAST Aurelia docs for future community members.

@EisenbergEffect so in 2 while the field is blank the VM technically still has the correct value bound to the variable on load (so if the constructor sets it to 'test' it is still set to test even though the box shows blank). In 1 the VM is actually overwritten with the blank value on load so if it is set to something on the constructor by load time it will be changed to blank.

I'm also facing this issue when using FAST in Aurelia 2. Any progress so far ? Anything I could do to help in this area ?

We haven't had a chance to look into it yet on our side. Always open to a fix from the community if you have a chance to look into it @ben-girardet

Well it's an issue I'm also facing and I would be glad to help in a sense. But to be honest I'm not sure if it's in my reach as I'm not familiar with the binding mechanism of either Aurelia or FAST.

As a start, is it something that should be fixed from Fast or AU2 codebase?

I think it should be a plugin to teach Aurelia how to observe fast elements. Same way with how we teaching Aurelia to observe native html elements

Thanks @bigopon for jumping on here. Could you point me towards the code where Aurelia is taught how to bind native elements ?

As a naive question: as FAST are web components, this "teaching Aurelia how to bind with FAST"; is it purely related to FAST or once done it should then work with any web components ?

For two-way there may need to be something Aurelia-specific. But I think there's probably a legitimate bug somewhere since one-way and one-time don't work either, and there shouldn't be anything special there if I recall.

I don't know if it helps to locate the issue going here, but here is what I discovered in a scenario such as:

<fast-text-field value.two-way="myValue"></fast-text-field>
  1. If myValue is already set at the moment the <fast-text-field> is attached => then this value is not displayed in the input of the fast-text-field. But as @mitchcapper said the VM doesn't loose the value, so if the user doesn't interact with the field, the VM still holds the right value.
  2. Of course, given the previous statement, if the user starts writing in the field then we loose the originally bound value
  3. But if the VM changes myValue after the <fast-text-field> is already created, then the observation works 100% fine (two-ways).

In other words, in my app I've been able to make it work 100% by adding a timeout (100ms) to wait for the view to be created before to bind the values (exemple below).

// my-view-model.ts
// ...
  public async enter(parameters: {topicId?: string}): Promise<void> {
    if (parameters.topicId) {
      const topic = await this.getTopic(parameters.topicId);
      setTimeout(() => {
        // binding the value after a 100ms timeout
        this.title = topic.title;
        this.description = topic.description;
      }, 100);
    }
  }
// ...

Of course I don't want to keep this timeout so I'm looking for a proper solution. But hopefully this can give us a hint to find out what's going on here...

A two way binding, declared via implicit syntax .bind, from template syntax needs 2 step process:

  • translate the syntax into a two way binding instruction (to-view & from-view)
  • use the right observation for from-view binding instruction

To fully enable two way binding with Aurelia, there's need a plugin to teach Aurelia to:

  1. understand that <fast-text-field value.bind/> should be treated as value.two-way
  2. understand how to observe a value property from a <fast-text-field/>

If we only ever write <fast-text-field value.two-way/>, or <fast-text-field value.from-view />, then we don't need to do (1), but I guess we want to write <fast-text-field value.bind />, so the to-be-implemented plugin should do:

  • plug an override into the syntax analyzer to teach it to transform a value.bind on a <fast-text-field/> to value.two-way
  • plug an adapter into the observer locator to teach it how to observe value property on a <fast-text-field/>

An example of this is in aurelia-ux, you can have a look at how it is setup in @aurelia-ux/core here https://github.com/aurelia/ux/blob/c3785dc683ba471ae2f24ca49d4f98c905625816/packages/core/src/aurelia-ux.ts#L59

I gave it a try by looking at this in aurelia-ux. Now here are some questions to keep me going:

  1. Aurelia UX is Au1 and I'm trying to do this for Au2. I have tried to translate the code of Aurelia UX for AU2 (see below) but there are things I don't understand. I'm mainly concerned by the SyntaxInterpreter (I couldn't find how to intercept it) and the ValueAttributeObserver (it requires a node parameter where it seems to me that it should be the element but I'm not sure).
  2. If you look at my comment above the would notice that the two-way binding nearly works 100%. Seems that it could be a "timing" issue rather than being an incapacity of AU2 to observe the property changes (because it does observer correctly, just "too late" kindof)
// aurelia-fast-adapter.ts
import { IObserverLocator, BindingMode, LifecycleFlags, IScheduler } from 'aurelia';
import { ISyntaxInterpreter } from '@aurelia/jit';
import { IBindingTargetObserver, IDOM } from '@aurelia/runtime';
import { ValueAttributeObserver, EventSubscriber } from '@aurelia/runtime-html';

export type GetElementObserver = (
  obj: Element,
  propertyName: string,
  observerLocator: IObserverLocator,
  descriptor?: PropertyDescriptor | null) => IBindingTargetObserver | null;

export interface FastElementObserverAdapter {
  tagName: string;
  properties: Record<string, FastElementPropertyObserver>;
}

export interface FastElementPropertyObserver {
  defaultBindingMode: BindingMode;
  getObserver: GetElementObserver;
}

export class AureliaFastAdapter {

  private bindingModeIntercepted = false;
  private adapterCreated = false;
  private adapters: Record<string, FastElementObserverAdapter> = {};

  public constructor(private observerLocator: IObserverLocator, private syntaxInterpreter: ISyntaxInterpreter, private scheduler: IScheduler, private dom: IDOM) {

  }

  private createAdapter() {
    this.observerLocator.addAdapter({
      getObserver: (flags: LifecycleFlags, obj: Element, propertyName: string, descriptor: PropertyDescriptor) => {
        if (obj instanceof Element) {
          const tagName = obj.getAttribute('as-element') || obj.tagName;
          const elAdapters = this.adapters[tagName];
          if (!elAdapters) {
            return null;
          }
          const propertyAdapter = elAdapters.properties[propertyName];
          if (propertyAdapter) {
            const observer = propertyAdapter.getObserver(obj, propertyName, this.observerLocator, descriptor);
            if (observer) {
              return observer;
            }
          }
        }
        return null;
      }
    });
  }

  private getOrCreateFastElementAdapters(tagName: string): FastElementObserverAdapter {
    if (!this.adapterCreated) {
      this.createAdapter();
      this.adapterCreated = true;
    }
    const adapters = this.adapters;
    let elementAdapters = adapters[tagName] || adapters[tagName.toLowerCase()];
    if (!elementAdapters) {
      elementAdapters = adapters[tagName] = adapters[tagName.toLowerCase()] = { tagName, properties: {} };
    }
    return elementAdapters;
  }

  private interceptDetermineDefaultBindingMode(): void {
    // I totally need help here
    this.syntaxInterpreter.add({
      pattern: '',
      symbols: ''
    });
  }

  public addFastElementObserverAdapter(tagName: string, properties: Record<string, FastElementPropertyObserver>): void {
    if (!this.adapterCreated) {
      this.createAdapter();
      this.adapterCreated = true;
    }
    const elementAdapters = this.getOrCreateFastElementAdapters(tagName);
    Object.assign(elementAdapters.properties, properties);
  }

  public registerFastElementConfig(observerAdapter: FastElementObserverAdapter): void {
    if (!this.bindingModeIntercepted) {
      this.interceptDetermineDefaultBindingMode();
      this.bindingModeIntercepted = true;
    }
    this.addFastElementObserverAdapter(observerAdapter.tagName.toUpperCase(), observerAdapter.properties);
  }

  public registerFastElementTwoWaysProperties(tagname: string, properties: string[]): void {
    const config: FastElementObserverAdapter = {
      tagName: tagname,
      properties: {}
    };
    for (const property of properties) {
      const propertyConfig: FastElementPropertyObserver = {
        defaultBindingMode: BindingMode.twoWay,
        getObserver(element: Element) {
          const node: any = element; // have no idea if this is correct
          return new ValueAttributeObserver(this.scheduler, LifecycleFlags.none, new EventSubscriber(this.dom, ['change']), node, property);
        }
      }
      config.properties[property] = propertyConfig;
    }
  }
}

@ben-girardet thanks for the clarification. I forgot you meant to ask for v2, not v1. For v2, I think having .two-way should work fine. I'm doing a small refactoring around this area, will get this PR through soon, and will get back here.

@mitchcapper @ben-girardet

This is now can be solved with proper API in Aurelia v2. Please have a read at the following links for the documentation:

@bigopon Do you have any interest in contributing parallel documentation into the FAST Aurelia 2 docs article?

@EisenbergEffect sure I can do that, unless @ben-girardet wants to give a go, since he's actively using the integration

Was this page helpful?
0 / 5 - 0 ratings

Related issues

PhilippSonntagORGADATA picture PhilippSonntagORGADATA  路  4Comments

MarcSkovMadsen picture MarcSkovMadsen  路  6Comments

gqio picture gqio  路  3Comments

LucSuy picture LucSuy  路  6Comments

MarcSkovMadsen picture MarcSkovMadsen  路  4Comments